diff --git a/backend/Readme.md b/backend/Readme.md new file mode 100644 index 0000000..7b017c7 --- /dev/null +++ b/backend/Readme.md @@ -0,0 +1,487 @@ +# Monaco Backend - Code Execution Service + +## Table of Contents + +1. Introduction +2. Architecture +3. Installation +4. API Reference +5. Code Execution +6. Job Queue System +7. Language Support +8. Security Considerations +9. Configuration +10. Testing +11. Performance Tuning +12. Troubleshooting + +## Introduction + +Monaco is a secure, containerized code execution backend service designed to run user-submitted code in multiple programming languages. It features a job queue system to manage execution resources, containerized execution environments for security, and a RESTful API for submission and monitoring. + +**Key Features:** +- Multi-language support (Python, Java, C, C++) +- Secure containerized execution using Docker +- Resource limiting to prevent abuse +- Job queuing for managing concurrent executions +- Detailed execution statistics and monitoring +- Support for user input via stdin +- CORS support for browser-based clients + +## Architecture + +### Component Overview + +Monaco follows a layered architecture with the following key components: + +1. **HTTP Handlers** (handler package) - Processes incoming HTTP requests +2. **Execution Service** (service package) - Manages code execution in containers +3. **Job Queue** (queue package) - Controls concurrent execution +4. **Data Models** (model package) - Defines data structures + +### Request Flow + +1. Client submits code via `/submit` endpoint +2. Request is validated and assigned a unique ID +3. Submission is added to the job queue +4. Worker picks job from queue when available +5. Code is executed in appropriate Docker container +6. Results are stored and available via `/result` endpoint + +### Dependency Diagram + +``` +Client Request → HTTP Handlers → Execution Service → Job Queue → Docker Containers + ↑ + Data Models +``` + +## Installation + +### Prerequisites + +- Go 1.22+ +- Docker Engine +- Docker images for supported languages: + - `python:3.9` + - `eclipse-temurin:11-jdk-alpine` + - `gcc:latest` + +### Setup Instructions + +1. Clone the repository: + ```bash + git clone https://github.com/arnab-afk/monaco.git + cd monaco/backend + ``` + +2. Install Go dependencies: + ```bash + go mod download + ``` + +3. Build the application: + ```bash + go build -o monaco main.go + ``` + +4. Run the service: + ```bash + ./monaco + ``` + +The service will start on port 8080 by default. + +## API Reference + +### Endpoints + +#### `POST /submit` + +Submits code for execution. + +**Request Body:** +```json +{ + "language": "python", // Required: "python", "java", "c", or "cpp" + "code": "print('Hello, World!')", // Required: source code to execute + "input": "optional input string" // Optional: input to stdin +} +``` + +**Response:** +```json +{ + "id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1" // Unique ID for this submission +} +``` + +**Status Codes:** +- 202 Accepted - Code accepted for execution +- 400 Bad Request - Invalid request (e.g., unsupported language) + +#### `GET /status?id={submissionId}` + +Checks the status of a submission. + +**Response:** +```json +{ + "id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1", + "status": "completed", // "pending", "queued", "running", "completed", "failed" + "queuedAt": "2025-03-25T14:30:00Z", + "startedAt": "2025-03-25T14:30:01Z", // Only present if status is "running", "completed", or "failed" + "completedAt": "2025-03-25T14:30:02Z", // Only present if status is "completed" or "failed" + "executionTime": 1000 // Execution time in milliseconds (only if completed) +} +``` + +**Status Codes:** +- 200 OK - Status retrieved successfully +- 400 Bad Request - Missing ID parameter +- 404 Not Found - Submission with given ID not found + +#### `GET /result?id={submissionId}` + +Gets the execution result of a submission. + +**Response:** +```json +{ + "id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1", + "status": "completed", + "language": "python", + "output": "Hello, World!", + "queuedAt": "2025-03-25T14:30:00Z", + "startedAt": "2025-03-25T14:30:01Z", + "completedAt": "2025-03-25T14:30:02Z", + "executionTime": 1000, + "executionTimeFormatted": "1.0s", + "totalTime": 2000, + "totalTimeFormatted": "2.0s" +} +``` + +**Status Codes:** +- 200 OK - Result retrieved successfully +- 400 Bad Request - Missing ID parameter +- 404 Not Found - Submission with given ID not found + +#### `GET /queue-stats` + +Gets statistics about the job queue. + +**Response:** +```json +{ + "queue_stats": { + "queue_length": 5, + "max_workers": 3, + "running_jobs": 3 + }, + "submissions": 42 +} +``` + +## Code Execution + +### Execution Process + +1. **Language Detection**: The system identifies the programming language of the submission. +2. **Environment Setup**: A temporary directory is created for compiled languages. +3. **Container Setup**: Docker containers are configured with resource limits. +4. **Compilation**: For compiled languages (Java, C, C++), code is compiled first. +5. **Execution**: The program is executed with the provided input. +6. **Resource Monitoring**: Memory and CPU usage are limited during execution. +7. **Result Collection**: Output and errors are captured and stored. + +### Language-Specific Processing + +#### Python +- Directly executes Python code using the `-c` flag +- Uses `python:3.9` Docker image +- Resource limits: 100MB memory, 10% CPU + +#### Java +- Detects class name using regex pattern matching +- Compiles with `javac` and runs with optimized JVM settings +- Uses `eclipse-temurin:11-jdk-alpine` Docker image +- Resource limits: 400MB memory, 50% CPU +- JVM flags: `-XX:+TieredCompilation`, `-XX:TieredStopAtLevel=1`, `-Xverify:none` + +#### C/C++ +- Saves code to a file in a temporary directory +- Compiles with `gcc`/`g++` and runs the executable +- Uses `gcc:latest` Docker image +- Resource limits: 100MB memory, 10% CPU + +### Timeout Handling + +All executions have a timeout limit: +- Python: 10 seconds +- Java: 15 seconds +- C/C++: 10 seconds + +If execution exceeds this limit, the process is killed and an error is returned. + +## Job Queue System + +### Worker Pool + +Monaco uses a worker pool to manage concurrent code executions: + +- Default pool size: 20 workers (configurable) +- Maximum queue capacity: 100 jobs +- FIFO (First-In-First-Out) processing order + +### Job Lifecycle + +1. **Creation**: Job created when code is submitted +2. **Queuing**: Job added to queue with `queued` status +3. **Execution**: Worker picks job from queue and changes status to `running` +4. **Completion**: Job finishes with either `completed` or `failed` status + +### Performance Metrics + +The queue tracks and reports: +- Current queue length +- Number of running jobs +- Maximum worker count +- Total number of submissions + +## Language Support + +### Python +- **Version**: Python 3.9 +- **Input Handling**: Direct stdin piping +- **Limitations**: No file I/O, no package imports outside standard library + +### Java +- **Version**: Java 11 (Eclipse Temurin) +- **Class Detection**: Extracts class name from code using regex +- **Memory Settings**: 64MB min heap, 256MB max heap +- **Best Practices**: Use `public class` with the main method + +### C +- **Version**: Latest GCC +- **Compilation Flags**: Default GCC settings +- **Execution**: Compiled binary + +### C++ +- **Version**: Latest G++ +- **Standard**: C++17 +- **Execution**: Compiled binary + +## Security Considerations + +### Containerization + +All code execution happens within isolated Docker containers with: +- No network access (`--network=none`) +- Limited CPU and memory resources +- Limited file system access +- No persistent storage + +### Resource Limiting + +- **Memory Limits**: 100-400MB depending on language +- **CPU Limits**: 10-50% of CPU depending on language +- **File Descriptors**: Limited to 64 for Python +- **Execution Time**: Enforced timeouts (10-15 seconds) + +### Known Limitations + +- Container escape vulnerabilities +- Docker daemon security depends on host configuration +- Resource limits can be circumvented with certain techniques + +## Configuration + +The service can be configured through environment variables: + +- `PORT`: HTTP port (default: 8080) +- `MAX_WORKERS`: Maximum concurrent executions (default: 3) +- `QUEUE_SIZE`: Maximum queue size (default: 100) +- `DEFAULT_LANGUAGE`: Default language if none specified (default: "python") + +## Testing + +### Unit Tests + +Run unit tests with: +```bash +go test ./... +``` + +# Monaco Backend Test Plan + +## Overview +This test plan outlines the testing approach for the Monaco code execution backend service. + +## Test Environment +- Development: Local workstations with Docker and Go +- Testing: Dedicated test server with Docker +- Production-like: Staging environment with similar resources to production + +## Test Types + +### Unit Tests +- **Purpose**: Verify individual components work as expected +- **Components to Test**: + - Handler package + - Queue package + - Execution service + - Models +- **Tools**: Go testing framework + +### Integration Tests +- **Purpose**: Verify components work together correctly +- **Focus Areas**: + - API endpoints + - End-to-end code execution flow + - Error handling +- **Tools**: Go testing framework, HTTP test utilities + +### Load Tests +- **Purpose**: Verify system performance under load +- **Scenarios**: + - Concurrent submissions + - Mixed language workloads + - Queue saturation +- **Metrics**: + - Request throughput + - Response times + - Success rates + - Resource utilization +- **Tools**: Custom Python test scripts + +## Test Data +- Simple programs in each language +- Programs with input requirements +- Programs with compile errors +- Programs with runtime errors +- Programs with timeouts + +## Test Execution +1. Run unit tests on every code change +2. Run integration tests before merging to main branch +3. Run load tests weekly and before major releases + +## Success Criteria +- All unit tests pass +- Integration tests complete successfully +- Load tests show acceptable performance metrics: + - 95% of requests complete successfully + - 95th percentile response time < 5 seconds + - System can handle 20 concurrent users + +## Reporting +- Test results stored in CI/CD pipeline +- Performance metrics graphed over time +- Issues logged in GitHub issues + +### Load Testing + +A Python script (`test.py`) is included for load testing: +```bash +python test.py +``` + +This script sends 500 requests concurrently and reports performance metrics. + +### Manual Testing with Curl + +#### Python Example +```bash +curl -X POST http://localhost:8080/submit \ + -H "Content-Type: application/json" \ + -d '{ + "language": "python", + "code": "print(\"Hello, World!\")\nfor i in range(5):\n print(f\"Number: {i}\")", + "input": "" + }' +``` + +#### Java Example +```bash +curl -X POST http://localhost:8080/submit \ + -H "Content-Type: application/json" \ + -d '{ + "language": "java", + "code": "public class Solution {\n public static void main(String[] args) {\n System.out.println(\"Hello, World!\");\n for (int i = 0; i < 5; i++) {\n System.out.println(\"Number: \" + i);\n }\n }\n}", + "input": "" + }' +``` + +## Performance Tuning + +### Optimizing Worker Count + +The optimal worker count depends on: +- CPU cores available +- Memory available +- Docker container startup time + +For most single-server deployments, 3-5 workers is optimal. + +### Memory Considerations + +Each language has different memory requirements: +- Python: ~50-100MB per instance +- Java: ~200-400MB per instance +- C/C++: ~50-100MB per instance + +Calculate total memory needs as: `(Python instances × 100MB) + (Java instances × 400MB) + (C/C++ instances × 100MB)` + +### Disk Space Management + +Temporary files are cleaned up after execution, but with high request volumes, ensure adequate disk space for concurrent operations (approximately 1-5MB per request for compiled languages). + +## Troubleshooting + +### Common Issues + +#### Docker Connection Errors +``` +Error: Cannot connect to the Docker daemon +``` +**Solution**: Ensure Docker daemon is running with `systemctl start docker` or `docker --version` + +#### Permissions Issues +``` +Error: Permission denied while trying to connect to the Docker daemon socket +``` +**Solution**: Add user to docker group: `sudo usermod -aG docker $USER` + +#### Container Resource Limits +``` +Error: Container killed due to memory limit +``` +**Solution**: Increase memory limits in execution service or optimize submitted code + +#### File Not Found Errors +``` +Error: Failed to write Java file +``` +**Solution**: Check temporary directory permissions and disk space + +### Logs + +The service provides structured logs with prefixes for easier filtering: +- `[HTTP]` - API requests +- `[QUEUE]` - Queue operations +- `[WORKER-n]` - Worker activities +- `[EXEC-id]` - Execution details +- `[PYTHON/JAVA/C/CPP-id]` - Language-specific logs +- `[TIMEOUT-id]` - Timeout events +- `[RESULT-id]` - Execution results + +--- + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. diff --git a/backend/go.mod b/backend/go.mod index 0dbb9aa..54f54af 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -2,75 +2,13 @@ module github.com/arnab-afk/monaco go 1.22.3 +require github.com/stretchr/testify v1.9.0 + require ( - github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect - github.com/distribution/reference v0.6.0 // indirect - github.com/docker/cli v26.1.5+incompatible // indirect - github.com/docker/docker v26.1.5+incompatible // indirect - github.com/docker/go-connections v0.4.0 // indirect - github.com/docker/go-units v0.5.0 // indirect - github.com/expr-lang/expr v1.16.5 // indirect - github.com/fatih/color v1.16.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/go-logr/logr v1.4.1 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.19.0 // indirect - github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt v3.2.2+incompatible // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/jmoiron/sqlx v1.3.5 // indirect - github.com/knadh/koanf/maps v0.1.1 // indirect - github.com/knadh/koanf/parsers/toml v0.1.0 // indirect - github.com/knadh/koanf/providers/env v0.1.0 // indirect - github.com/knadh/koanf/providers/file v0.1.0 // indirect - github.com/knadh/koanf/v2 v2.1.1 // indirect - github.com/labstack/echo/v4 v4.12.0 // indirect - github.com/labstack/gommon v0.4.2 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/lib/pq v1.10.9 // indirect - github.com/lithammer/shortuuid/v4 v4.0.0 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect - github.com/rabbitmq/amqp091-go v1.9.0 // indirect - github.com/rs/zerolog v1.32.0 // indirect - github.com/runabol/code-execution-demo v0.0.0-20231001172254-4b23df61090b // indirect - github.com/runabol/tork v0.1.98 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/shirou/gopsutil/v3 v3.24.3 // indirect - github.com/shoenig/go-m1cpu v0.1.6 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect - github.com/urfave/cli/v2 v2.27.2 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.2 // indirect - github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect - go.opentelemetry.io/otel v1.26.0 // indirect - go.opentelemetry.io/otel/metric v1.26.0 // indirect - go.opentelemetry.io/otel/trace v1.26.0 // indirect - golang.org/x/crypto v0.22.0 // indirect - golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.5.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/pretty v0.3.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect -) \ No newline at end of file +) diff --git a/backend/go.sum b/backend/go.sum index 8c94826..47570c2 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,214 +1,23 @@ -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v26.1.5+incompatible h1:NxXGSdz2N+Ibdaw330TDO3d/6/f7MvHuiMbuFaIQDTk= -github.com/docker/cli v26.1.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v26.1.5+incompatible h1:NEAxTwEjxV6VbBMBoGG3zPqbiJosIApZjxlbrG9q3/g= -github.com/docker/docker v26.1.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/expr-lang/expr v1.16.5 h1:m2hvtguFeVaVNTHj8L7BoAyt7O0PAIBaSVbjdHgRXMs= -github.com/expr-lang/expr v1.16.5/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/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/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= -github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= -github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.3.0/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/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= -github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= -github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= -github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI= -github.com/knadh/koanf/parsers/toml v0.1.0/go.mod h1:yUprhq6eo3GbyVXFFMdbfZSo928ksS+uo0FFqNMnO18= -github.com/knadh/koanf/providers/env v0.1.0 h1:LqKteXqfOWyx5Ab9VfGHmjY9BvRXi+clwyZozgVRiKg= -github.com/knadh/koanf/providers/env v0.1.0/go.mod h1:RE8K9GbACJkeEnkl8L/Qcj8p4ZyPXZIQ191HJi44ZaQ= -github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c= -github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA= -github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM= -github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= -github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= -github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= -github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c= -github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/rabbitmq/amqp091-go v1.9.0 h1:qrQtyzB4H8BQgEuJwhmVQqVHB9O4+MNDJCCAcpc3Aoo= -github.com/rabbitmq/amqp091-go v1.9.0/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= -github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= -github.com/runabol/code-execution-demo v0.0.0-20231001172254-4b23df61090b h1:hSYt/eRkKwJ3C40aHMJJLlJg+ysSq7FvnTYYFymouhY= -github.com/runabol/code-execution-demo v0.0.0-20231001172254-4b23df61090b/go.mod h1:FRPaDaWfFOIYN5QjmQH4aH/F9Jc2CXtbzwB8c51sg4s= -github.com/runabol/tork v0.1.97 h1:XZo39ZXruiDipN3xdgQpKrLjiyDTHL8vjEtHHA8r5+U= -github.com/runabol/tork v0.1.97/go.mod h1:EuJuNGc5raepZ5fFE9meC4QbUVaWHedF5P8KEYFHBX4= -github.com/runabol/tork v0.1.98 h1:R/QkBTSF4sQLnW8nq5vlIPwtHuwllDBYjJMNlQ4vTkc= -github.com/runabol/tork v0.1.98/go.mod h1:EuJuNGc5raepZ5fFE9meC4QbUVaWHedF5P8KEYFHBX4= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shirou/gopsutil/v3 v3.24.3 h1:eoUGJSmdfLzJ3mxIhmOAhgKEKgQkeOwKpz1NbhVnuPE= -github.com/shirou/gopsutil/v3 v3.24.3/go.mod h1:JpND7O217xa72ewWz9zN2eIIkPWsDN/3pl0H8Qt0uwg= -github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= -github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= -github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +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/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= -github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= -github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc= -go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= -go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= -go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= -go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= -go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= -go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= -go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= \ No newline at end of file +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/handler/handler_test.go b/backend/handler/handler_test.go new file mode 100644 index 0000000..81c37dc --- /dev/null +++ b/backend/handler/handler_test.go @@ -0,0 +1,154 @@ +package handler + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/arnab-afk/monaco/model" + "github.com/stretchr/testify/assert" +) + +func TestSubmitHandler(t *testing.T) { + h := NewHandler() + + // Test valid Python submission + body := map[string]string{ + "language": "python", + "code": "print('Hello, World!')", + } + bodyBytes, _ := json.Marshal(body) + req := httptest.NewRequest("POST", "/submit", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + h.SubmitHandler(w, req) + + assert.Equal(t, http.StatusAccepted, w.Code) + var response map[string]string + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.NotEmpty(t, response["id"]) + + // Test invalid language + body["language"] = "invalid" + bodyBytes, _ = json.Marshal(body) + req = httptest.NewRequest("POST", "/submit", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + w = httptest.NewRecorder() + + h.SubmitHandler(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "Unsupported language") +} + +func TestStatusHandler(t *testing.T) { + h := NewHandler() + + // Create a test submission + submission := &model.CodeSubmission{ + ID: "test-id", + Language: "python", + Code: "print('Hello')", + Status: "completed", + QueuedAt: time.Now().Add(-2 * time.Second), + StartedAt: time.Now().Add(-1 * time.Second), + CompletedAt: time.Now(), + Output: "Hello", + } + + h.submissions["test-id"] = submission + + // Test valid status request + req := httptest.NewRequest("GET", "/status?id=test-id", nil) + w := httptest.NewRecorder() + + h.StatusHandler(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "test-id", response["id"]) + assert.Equal(t, "completed", response["status"]) + + // Test missing ID + req = httptest.NewRequest("GET", "/status", nil) + w = httptest.NewRecorder() + + h.StatusHandler(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "ID is required") + + // Test non-existent ID + req = httptest.NewRequest("GET", "/status?id=nonexistent", nil) + w = httptest.NewRecorder() + + h.StatusHandler(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) + assert.Contains(t, w.Body.String(), "Submission not found") +} + +func TestResultHandler(t *testing.T) { + h := NewHandler() + + // Create a test submission + submission := &model.CodeSubmission{ + ID: "test-id", + Language: "python", + Code: "print('Hello')", + Status: "completed", + QueuedAt: time.Now().Add(-2 * time.Second), + StartedAt: time.Now().Add(-1 * time.Second), + CompletedAt: time.Now(), + Output: "Hello", + } + + h.submissions["test-id"] = submission + + // Test valid result request + req := httptest.NewRequest("GET", "/result?id=test-id", nil) + w := httptest.NewRecorder() + + h.ResultHandler(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "test-id", response["id"]) + assert.Equal(t, "completed", response["status"]) + assert.Equal(t, "Hello", response["output"]) +} + +func TestQueueStatsHandler(t *testing.T) { + h := NewHandler() + + // Add some test submissions + h.submissions["test-id1"] = &model.CodeSubmission{ID: "test-id1"} + h.submissions["test-id2"] = &model.CodeSubmission{ID: "test-id2"} + + req := httptest.NewRequest("GET", "/queue-stats", nil) + w := httptest.NewRecorder() + + h.QueueStatsHandler(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + + stats, ok := response["queue_stats"].(map[string]interface{}) + assert.True(t, ok) + assert.Contains(t, stats, "queue_length") + assert.Contains(t, stats, "max_workers") + assert.Contains(t, stats, "running_jobs") + + assert.Equal(t, float64(2), response["submissions"]) +} \ No newline at end of file diff --git a/backend/model/submission_test.go b/backend/model/submission_test.go new file mode 100644 index 0000000..376150b --- /dev/null +++ b/backend/model/submission_test.go @@ -0,0 +1,71 @@ +package model + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestCodeSubmissionSerialization(t *testing.T) { + // Create a submission + now := time.Now() + submission := CodeSubmission{ + ID: "test-id", + Code: "print('Hello, World!')", + Language: "python", + Input: "test input", + Status: "completed", + QueuedAt: now.Add(-2 * time.Second), + StartedAt: now.Add(-1 * time.Second), + CompletedAt: now, + Output: "Hello, World!", + } + + // Serialize to JSON + jsonBytes, err := json.Marshal(submission) + assert.NoError(t, err) + assert.NotNil(t, jsonBytes) + + // Deserialize back + var decoded CodeSubmission + err = json.Unmarshal(jsonBytes, &decoded) + assert.NoError(t, err) + + // Verify fields match + assert.Equal(t, submission.ID, decoded.ID) + assert.Equal(t, submission.Code, decoded.Code) + assert.Equal(t, submission.Language, decoded.Language) + assert.Equal(t, submission.Input, decoded.Input) + assert.Equal(t, submission.Status, decoded.Status) + assert.Equal(t, submission.Output, decoded.Output) + + // Time fields need special handling due to JSON serialization + assert.Equal(t, submission.QueuedAt.Format(time.RFC3339), decoded.QueuedAt.Format(time.RFC3339)) + assert.Equal(t, submission.StartedAt.Format(time.RFC3339), decoded.StartedAt.Format(time.RFC3339)) + assert.Equal(t, submission.CompletedAt.Format(time.RFC3339), decoded.CompletedAt.Format(time.RFC3339)) +} + +func TestCodeSubmissionDefaults(t *testing.T) { + // Test that zero time values work correctly + submission := CodeSubmission{ + ID: "test-id", + Code: "print('Hello')", + Language: "python", + Status: "pending", + } + + assert.True(t, submission.QueuedAt.IsZero()) + assert.True(t, submission.StartedAt.IsZero()) + assert.True(t, submission.CompletedAt.IsZero()) + + // Test JSON marshaling with zero time values + jsonBytes, err := json.Marshal(submission) + assert.NoError(t, err) + + // The zero time values should still be included in the JSON + jsonStr := string(jsonBytes) + assert.Contains(t, jsonStr, `"id":"test-id"`) + assert.Contains(t, jsonStr, `"status":"pending"`) +} diff --git a/backend/queue/queue_test.go b/backend/queue/queue_test.go new file mode 100644 index 0000000..529781d --- /dev/null +++ b/backend/queue/queue_test.go @@ -0,0 +1,112 @@ +package queue + +import ( + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +// Mock job for testing +type MockJob struct { + executed bool + executeTime time.Duration + mu sync.Mutex +} + +func (j *MockJob) Execute() { + j.mu.Lock() + defer j.mu.Unlock() + + time.Sleep(j.executeTime) + j.executed = true +} + +func (j *MockJob) IsExecuted() bool { + j.mu.Lock() + defer j.mu.Unlock() + return j.executed +} + +func TestJobQueueCreation(t *testing.T) { + // Test with different numbers of workers + jq := NewJobQueue(5) + assert.NotNil(t, jq) + assert.Equal(t, 5, jq.maxWorkers) + + stats := jq.QueueStats() + assert.Equal(t, 0, stats["queue_length"]) + assert.Equal(t, 5, stats["max_workers"]) + assert.Equal(t, 0, stats["running_jobs"]) +} + +func TestJobExecution(t *testing.T) { + jq := NewJobQueue(2) + + // Create test jobs + job1 := &MockJob{executeTime: 10 * time.Millisecond} + job2 := &MockJob{executeTime: 10 * time.Millisecond} + + // Enqueue jobs + jq.Enqueue(job1) + jq.Enqueue(job2) + + // Wait for execution + time.Sleep(50 * time.Millisecond) + + // Verify both jobs executed + assert.True(t, job1.IsExecuted()) + assert.True(t, job2.IsExecuted()) +} + +func TestConcurrentJobsExecution(t *testing.T) { + // Test that only maxWorkers jobs run concurrently + jq := NewJobQueue(2) + + var mu sync.Mutex + runningCount := 0 + maxObservedRunning := 0 + + wg := sync.WaitGroup{} + + // Create long running jobs to test concurrency + for i := 0; i < 5; i++ { + wg.Add(1) + job := &MockJob{ + executeTime: 100 * time.Millisecond, + } + + // Wrap the job to monitor concurrency + wrappedJob := JobFunc(func() { + mu.Lock() + runningCount++ + if runningCount > maxObservedRunning { + maxObservedRunning = runningCount + } + mu.Unlock() + + job.Execute() + + mu.Lock() + runningCount-- + mu.Unlock() + wg.Done() + }) + + jq.Enqueue(wrappedJob) + } + + wg.Wait() + jq.Stop() + + // Verify max concurrent jobs is respected + assert.LessOrEqual(t, maxObservedRunning, 2) +} + +// Define JobFunc type for easier job creation in tests +type JobFunc func() + +func (f JobFunc) Execute() { + f() +} diff --git a/backend/service/execution_test.go b/backend/service/execution_test.go new file mode 100644 index 0000000..f8409e5 --- /dev/null +++ b/backend/service/execution_test.go @@ -0,0 +1,115 @@ +package service + +import ( + "os" + "testing" + "time" + + "github.com/arnab-afk/monaco/model" + "github.com/stretchr/testify/assert" +) + +// TestExecutionServiceCreation tests that the service is created properly +func TestExecutionServiceCreation(t *testing.T) { + service := NewExecutionService() + assert.NotNil(t, service) + assert.NotNil(t, service.queue) +} + +// TestExtractClassName tests the class name extraction for Java code +func TestExtractClassName(t *testing.T) { + tests := []struct { + name string + code string + expected string + }{ + { + name: "Public class", + code: "public class MyClass { public static void main(String[] args) {} }", + expected: "MyClass", + }, + { + name: "Regular class", + code: "class RegularClass { public static void main(String[] args) {} }", + expected: "RegularClass", + }, + { + name: "Multiple classes", + code: "class Class1 {} public class MainClass {} class Class2 {}", + expected: "MainClass", + }, + { + name: "No class", + code: "// Just a comment", + expected: "Solution", // Default class name + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := extractClassName(tt.code) + assert.Equal(t, tt.expected, result) + }) + } +} + +// MockDockerExec is a function that can be used to mock Docker exec commands +type MockDockerExec func(cmd string, args ...string) ([]byte, error) + +// TestUpdateSubmissionResult tests the submission result update logic +func TestUpdateSubmissionResult(t *testing.T) { + service := NewExecutionService() + + // Test successful execution + submission := &model.CodeSubmission{ + ID: "test-id", + Status: "running", + StartedAt: time.Now().Add(-500 * time.Millisecond), + QueuedAt: time.Now().Add(-1 * time.Second), + } + + output := []byte("Hello, World!") + service.updateSubmissionResult(submission, output, nil) + + assert.Equal(t, "completed", submission.Status) + assert.Equal(t, "Hello, World!", submission.Output) + assert.False(t, submission.CompletedAt.IsZero()) + + // Test failed execution + submission = &model.CodeSubmission{ + ID: "test-id-2", + Status: "running", + StartedAt: time.Now().Add(-500 * time.Millisecond), + QueuedAt: time.Now().Add(-1 * time.Second), + } + + output = []byte("Compilation error") + err := os.ErrInvalid // Any error will do for testing + service.updateSubmissionResult(submission, output, err) + + assert.Equal(t, "failed", submission.Status) + assert.Contains(t, submission.Output, "Compilation error") + assert.Contains(t, submission.Output, err.Error()) + assert.False(t, submission.CompletedAt.IsZero()) +} + +// TestCodeExecutionJob tests the job execution logic +func TestCodeExecutionJob(t *testing.T) { + service := NewExecutionService() + + submission := &model.CodeSubmission{ + ID: "test-id", + Language: "python", + Code: "print('test')", + Status: "queued", + QueuedAt: time.Now(), + } + + job := NewCodeExecutionJob(service, submission) + assert.NotNil(t, job) + assert.Equal(t, submission, job.submission) + assert.Equal(t, service, job.service) + + // We can't easily test the actual execution because it depends on Docker + // In a real test environment, you would mock the Docker calls +} \ No newline at end of file diff --git a/backend/tests/api_integration_test.go b/backend/tests/api_integration_test.go new file mode 100644 index 0000000..2483063 --- /dev/null +++ b/backend/tests/api_integration_test.go @@ -0,0 +1,195 @@ +package tests + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/arnab-afk/monaco/handler" + "github.com/stretchr/testify/assert" +) + +func setupTestServer() *httptest.Server { + h := handler.NewHandler() + + mux := http.NewServeMux() + mux.HandleFunc("/submit", h.SubmitHandler) + mux.HandleFunc("/status", h.StatusHandler) + mux.HandleFunc("/result", h.ResultHandler) + mux.HandleFunc("/queue-stats", h.QueueStatsHandler) + + return httptest.NewServer(mux) +} + +func TestAPIIntegration(t *testing.T) { + server := setupTestServer() + defer server.Close() + + // Test: Submit code, check status, and get results + // 1. Submit a Python job + submitURL := server.URL + "/submit" + body := map[string]string{ + "language": "python", + "code": "print('Hello, Integration Test!')", + } + + bodyBytes, _ := json.Marshal(body) + resp, err := http.Post(submitURL, "application/json", bytes.NewReader(bodyBytes)) + assert.NoError(t, err) + assert.Equal(t, http.StatusAccepted, resp.StatusCode) + + // Get the job ID + var submitResp map[string]string + json.NewDecoder(resp.Body).Decode(&submitResp) + resp.Body.Close() + + jobID := submitResp["id"] + assert.NotEmpty(t, jobID) + + // 2. Check status + statusURL := server.URL + "/status?id=" + jobID + + // Wait for job to complete (try multiple times) + var statusResp map[string]interface{} + maxRetries := 10 + + for i := 0; i < maxRetries; i++ { + resp, err = http.Get(statusURL) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + json.NewDecoder(resp.Body).Decode(&statusResp) + resp.Body.Close() + + // If job completed or failed, break + status, _ := statusResp["status"].(string) + if status == "completed" || status == "failed" { + break + } + + // Wait before next retry + time.Sleep(200 * time.Millisecond) + } + + // 3. Get results + resultURL := server.URL + "/result?id=" + jobID + resp, err = http.Get(resultURL) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var resultResp map[string]interface{} + json.NewDecoder(resp.Body).Decode(&resultResp) + resp.Body.Close() + + assert.Equal(t, jobID, resultResp["id"]) + assert.Contains(t, resultResp["output"], "Hello, Integration Test!") + + // 4. Check queue stats + statsURL := server.URL + "/queue-stats" + resp, err = http.Get(statsURL) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var statsResp map[string]interface{} + json.NewDecoder(resp.Body).Decode(&statsResp) + resp.Body.Close() + + assert.Contains(t, statsResp, "queue_stats") + assert.Contains(t, statsResp, "submissions") +} + +func TestMultipleLanguageSubmissions(t *testing.T) { + server := setupTestServer() + defer server.Close() + + // Test submissions for different languages + languages := []string{"python", "java", "c", "cpp"} + codes := map[string]string{ + "python": "print('Hello from Python')", + "java": "public class Solution { public static void main(String[] args) { System.out.println(\"Hello from Java\"); } }", + "c": "#include \nint main() { printf(\"Hello from C\\n\"); return 0; }", + "cpp": "#include \nint main() { std::cout << \"Hello from C++\" << std::endl; return 0; }", + } + + submitURL := server.URL + "/submit" + + for _, lang := range languages { + body := map[string]string{ + "language": lang, + "code": codes[lang], + } + + bodyBytes, _ := json.Marshal(body) + resp, err := http.Post(submitURL, "application/json", bytes.NewReader(bodyBytes)) + assert.NoError(t, err) + assert.Equal(t, http.StatusAccepted, resp.StatusCode) + + var submitResp map[string]string + json.NewDecoder(resp.Body).Decode(&submitResp) + resp.Body.Close() + + jobID := submitResp["id"] + assert.NotEmpty(t, jobID) + + // We don't wait for completion in this test + // This is just to verify submission acceptance for all languages + } +} + +func TestInputHandling(t *testing.T) { + server := setupTestServer() + defer server.Close() + + // Test code submission with input + submitURL := server.URL + "/submit" + body := map[string]string{ + "language": "python", + "code": "name = input('Enter name: ')\nprint('Hello, ' + name + '!')", + "input": "Integration Test", + } + + bodyBytes, _ := json.Marshal(body) + resp, err := http.Post(submitURL, "application/json", bytes.NewReader(bodyBytes)) + assert.NoError(t, err) + assert.Equal(t, http.StatusAccepted, resp.StatusCode) + + var submitResp map[string]string + json.NewDecoder(resp.Body).Decode(&submitResp) + resp.Body.Close() + + jobID := submitResp["id"] + assert.NotEmpty(t, jobID) + + // Wait for job to complete and check result + resultURL := server.URL + "/result?id=" + jobID + + // Poll for results + var resultResp map[string]interface{} + maxRetries := 10 + + for i := 0; i < maxRetries; i++ { + time.Sleep(300 * time.Millisecond) + + resp, err = http.Get(resultURL) + assert.NoError(t, err) + + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + continue + } + + json.NewDecoder(resp.Body).Decode(&resultResp) + resp.Body.Close() + + status, _ := resultResp["status"].(string) + if status == "completed" || status == "failed" { + break + } + } + + // Verify output contains the greeting with input + assert.Contains(t, resultResp["output"], "Hello, Integration Test!") +} diff --git a/backend/tests/load_test.py b/backend/tests/load_test.py new file mode 100644 index 0000000..1567de2 --- /dev/null +++ b/backend/tests/load_test.py @@ -0,0 +1,278 @@ +import requests +import concurrent.futures +import time +import statistics +import matplotlib.pyplot as plt +import numpy as np + +# Define the endpoint URLs +POST_URL = "http://localhost:8080/submit" +GET_URL_STATUS = "http://localhost:8080/status?id={}" +GET_URL_RESULT = "http://localhost:8080/result?id={}" +GET_URL_STATS = "http://localhost:8080/queue-stats" + +# Test payloads for different languages +PAYLOADS = { + "python": { + "language": "python", + "code": "print('Hello, Load Test!')", + }, + "java": { + "language": "java", + "code": "public class Solution { public static void main(String[] args) { System.out.println(\"Hello, Load Test!\"); } }", + }, + "c": { + "language": "c", + "code": "#include \nint main() { printf(\"Hello, Load Test!\\n\"); return 0; }", + }, + "cpp": { + "language": "cpp", + "code": "#include \nint main() { std::cout << \"Hello, Load Test!\" << std::endl; return 0; }", + } +} + +def send_request(language, index): + """Sends a POST request and returns (task_id, time_taken).""" + payload = PAYLOADS[language] + start_time = time.time() + try: + response = requests.post(POST_URL, json=payload, timeout=10) + end_time = time.time() + if response.status_code == 202: + return response.json().get("id"), end_time - start_time + else: + print(f"Request {index} failed with status {response.status_code}") + return None, end_time - start_time + except requests.exceptions.RequestException as e: + end_time = time.time() + print(f"Request {index} error: {e}") + return None, end_time - start_time + +def wait_for_result(task_id, index): + """Waits for a result and returns (result, time_taken).""" + if not task_id: + return None, 0 + + start_time = time.time() + max_retries = 30 + retry_interval = 0.5 # seconds + + for _ in range(max_retries): + try: + response = requests.get(GET_URL_RESULT.format(task_id), timeout=10) + if response.status_code == 200: + result = response.json() + if result.get("status") in ["completed", "failed"]: + end_time = time.time() + return result, end_time - start_time + time.sleep(retry_interval) + except requests.exceptions.RequestException as e: + print(f"Error checking result for task {index}: {e}") + + end_time = time.time() + print(f"Timed out waiting for result of task {index}") + return None, end_time - start_time + +def run_test(concurrency, requests_per_language): + """Runs a load test with the specified parameters.""" + languages = list(PAYLOADS.keys()) + all_results = {lang: [] for lang in languages} + submit_times = {lang: [] for lang in languages} + wait_times = {lang: [] for lang in languages} + success_rates = {lang: 0 for lang in languages} + + # Keep track of all submissions for each language + total_per_language = {lang: 0 for lang in languages} + successful_per_language = {lang: 0 for lang in languages} + + start_time = time.time() + + # Create a list of tasks + tasks = [] + for lang in languages: + for i in range(requests_per_language): + tasks.append((lang, i)) + + print(f"Running load test with {concurrency} concurrent connections") + print(f"Sending {requests_per_language} requests per language ({len(languages)} languages)") + + # Submit all tasks + task_ids = {} + with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as executor: + future_to_task = {executor.submit(send_request, lang, i): (lang, i) for lang, i in tasks} + for future in concurrent.futures.as_completed(future_to_task): + lang, i = future_to_task[future] + total_per_language[lang] += 1 + try: + task_id, submit_time = future.result() + if task_id: + task_ids[(lang, i)] = task_id + submit_times[lang].append(submit_time) + except Exception as e: + print(f"Error submitting {lang} task {i}: {e}") + + print(f"Submitted {len(task_ids)} tasks successfully") + + # Wait for all results + with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as executor: + future_to_task = {executor.submit(wait_for_result, task_ids.get((lang, i)), i): (lang, i) + for lang, i in tasks if (lang, i) in task_ids} + for future in concurrent.futures.as_completed(future_to_task): + lang, i = future_to_task[future] + try: + result, wait_time = future.result() + if result and result.get("status") == "completed": + successful_per_language[lang] += 1 + all_results[lang].append(result) + wait_times[lang].append(wait_time) + except Exception as e: + print(f"Error waiting for {lang} task {i}: {e}") + + end_time = time.time() + total_time = end_time - start_time + + # Calculate success rates + for lang in languages: + if total_per_language[lang] > 0: + success_rates[lang] = (successful_per_language[lang] / total_per_language[lang]) * 100 + else: + success_rates[lang] = 0 + + # Calculate statistics + stats = { + "total_time": total_time, + "requests_per_second": len(task_ids) / total_time if total_time > 0 else 0, + "success_rate": sum(success_rates.values()) / len(success_rates) if success_rates else 0, + "submit_times": { + lang: { + "avg": statistics.mean(times) if times else 0, + "min": min(times) if times else 0, + "max": max(times) if times else 0, + "p95": np.percentile(times, 95) if times else 0 + } for lang, times in submit_times.items() + }, + "wait_times": { + lang: { + "avg": statistics.mean(times) if times else 0, + "min": min(times) if times else 0, + "max": max(times) if times else 0, + "p95": np.percentile(times, 95) if times else 0 + } for lang, times in wait_times.items() + }, + "success_rates": success_rates + } + + return stats, all_results + +def print_stats(stats): + """Prints test statistics.""" + print("\n=== Load Test Results ===") + print(f"Total time: {stats['total_time']:.2f}s") + print(f"Requests per second: {stats['requests_per_second']:.2f}") + print(f"Overall success rate: {stats['success_rate']:.2f}%") + + print("\n== Submit Times (seconds) ==") + for lang, times in stats["submit_times"].items(): + print(f"{lang:<6}: avg={times['avg']:.4f}, min={times['min']:.4f}, max={times['max']:.4f}, p95={times['p95']:.4f}") + + print("\n== Wait Times (seconds) ==") + for lang, times in stats["wait_times"].items(): + print(f"{lang:<6}: avg={times['avg']:.4f}, min={times['min']:.4f}, max={times['max']:.4f}, p95={times['p95']:.4f}") + + print("\n== Success Rates ==") + for lang, rate in stats["success_rates"].items(): + print(f"{lang:<6}: {rate:.2f}%") + +def plot_results(stats): + """Creates visualizations of test results.""" + languages = list(stats["submit_times"].keys()) + + # Plot submit times + plt.figure(figsize=(12, 10)) + + plt.subplot(2, 2, 1) + plt.title("Average Submit Time by Language") + avg_times = [stats["submit_times"][lang]["avg"] for lang in languages] + plt.bar(languages, avg_times) + plt.ylabel("Time (seconds)") + + plt.subplot(2, 2, 2) + plt.title("Average Wait Time by Language") + avg_wait_times = [stats["wait_times"][lang]["avg"] for lang in languages] + plt.bar(languages, avg_wait_times) + plt.ylabel("Time (seconds)") + + plt.subplot(2, 2, 3) + plt.title("Success Rate by Language") + success_rates = [stats["success_rates"][lang] for lang in languages] + plt.bar(languages, success_rates) + plt.ylabel("Success Rate (%)") + plt.ylim(0, 100) + + plt.subplot(2, 2, 4) + plt.title("95th Percentile Wait Time by Language") + p95_times = [stats["wait_times"][lang]["p95"] for lang in languages] + plt.bar(languages, p95_times) + plt.ylabel("Time (seconds)") + + plt.tight_layout() + plt.savefig("load_test_results.png") + print("Results saved to load_test_results.png") + +def main(): + # Run tests with different concurrency levels + concurrency_levels = [10, 20, 30] + requests_per_language = 10 + + all_stats = [] + + for concurrency in concurrency_levels: + stats, results = run_test(concurrency, requests_per_language) + all_stats.append((concurrency, stats)) + print_stats(stats) + + # Create comparison visualization + plt.figure(figsize=(12, 8)) + + plt.subplot(2, 2, 1) + plt.title("Requests per Second vs Concurrency") + plt.plot([s[0] for s in all_stats], [s[1]["requests_per_second"] for s in all_stats], "o-") + plt.xlabel("Concurrency Level") + plt.ylabel("Requests per Second") + + plt.subplot(2, 2, 2) + plt.title("Success Rate vs Concurrency") + plt.plot([s[0] for s in all_stats], [s[1]["success_rate"] for s in all_stats], "o-") + plt.xlabel("Concurrency Level") + plt.ylabel("Success Rate (%)") + plt.ylim(0, 100) + + plt.subplot(2, 2, 3) + plt.title("Average Submit Time vs Concurrency") + for lang in PAYLOADS.keys(): + plt.plot([s[0] for s in all_stats], + [s[1]["submit_times"][lang]["avg"] for s in all_stats], + "o-", label=lang) + plt.xlabel("Concurrency Level") + plt.ylabel("Average Submit Time (s)") + plt.legend() + + plt.subplot(2, 2, 4) + plt.title("Average Wait Time vs Concurrency") + for lang in PAYLOADS.keys(): + plt.plot([s[0] for s in all_stats], + [s[1]["wait_times"][lang]["avg"] for s in all_stats], + "o-", label=lang) + plt.xlabel("Concurrency Level") + plt.ylabel("Average Wait Time (s)") + plt.legend() + + plt.tight_layout() + plt.savefig("concurrency_comparison.png") + print("Concurrency comparison saved to concurrency_comparison.png") + + # Plot detailed results for the highest concurrency test + plot_results(all_stats[-1][1]) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/backend/tmp/main.exe b/backend/tmp/main.exe deleted file mode 100644 index 10e1644..0000000 Binary files a/backend/tmp/main.exe and /dev/null differ