From ee66f1aa6f624c7de8dcbb4f25fca27cac18abf8 Mon Sep 17 00:00:00 2001 From: IrminDev Date: Thu, 15 Jan 2026 23:26:38 -0600 Subject: [PATCH 01/32] Added base for RabbitMQ integration and added worker application to eecute the code safely --- .gitignore | 2 + AGENTS.md | 888 ++++++++++++++++++ codehive-backend/docker-compose.yaml | 7 + codehive-worker/.gitattributes | 3 + codehive-worker/.gitignore | 37 + codehive-worker/build.gradle.kts | 35 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 45633 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + codehive-worker/gradlew | 248 +++++ codehive-worker/gradlew.bat | 93 ++ codehive-worker/settings.gradle.kts | 1 + .../codehive/worker/WorkerApplication.java | 13 + .../worker/config/DockerClientConfig.java | 5 + .../worker/config/RabbitMQConfig.java | 5 + .../listener/SubmissionListener.java | 5 + .../messaging/model/SubmissionMessage.java | 0 .../worker/sandbox/ExecutionResult.java | 5 + .../worker/sandbox/LanguageExecutor.java | 5 + .../worker/sandbox/SandboxExecutor.java | 5 + .../codehive/worker/sandbox/c/CExecutor.java | 11 + .../worker/sandbox/cpp/CPPExecutor.java | 10 + .../factory/LanguageExecutorFactory.java | 20 + .../worker/sandbox/java/JavaExecutor.java | 10 + .../worker/sandbox/python/PythonExecutor.java | 10 + .../src/main/resources/application.properties | 1 + .../worker/TestWorkerApplication.java | 11 + .../worker/TestcontainersConfiguration.java | 8 + .../worker/WorkerApplicationTests.java | 15 + 28 files changed, 1460 insertions(+) create mode 100644 AGENTS.md create mode 100644 codehive-worker/.gitattributes create mode 100644 codehive-worker/.gitignore create mode 100644 codehive-worker/build.gradle.kts create mode 100644 codehive-worker/gradle/wrapper/gradle-wrapper.jar create mode 100644 codehive-worker/gradle/wrapper/gradle-wrapper.properties create mode 100755 codehive-worker/gradlew create mode 100644 codehive-worker/gradlew.bat create mode 100644 codehive-worker/settings.gradle.kts create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/WorkerApplication.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/config/DockerClientConfig.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/config/RabbitMQConfig.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/SubmissionListener.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/messaging/model/SubmissionMessage.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/sandbox/ExecutionResult.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/sandbox/LanguageExecutor.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/sandbox/SandboxExecutor.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/sandbox/c/CExecutor.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/sandbox/cpp/CPPExecutor.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/sandbox/factory/LanguageExecutorFactory.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/sandbox/python/PythonExecutor.java create mode 100644 codehive-worker/src/main/resources/application.properties create mode 100644 codehive-worker/src/test/java/com/github/codehive/worker/TestWorkerApplication.java create mode 100644 codehive-worker/src/test/java/com/github/codehive/worker/TestcontainersConfiguration.java create mode 100644 codehive-worker/src/test/java/com/github/codehive/worker/WorkerApplicationTests.java diff --git a/.gitignore b/.gitignore index becb5bf..70d69f3 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,5 @@ logs/ npm-debug.log* yarn-debug.log* yarn-error.log* + +.crush/ \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d5a14c3 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,888 @@ +# AGENTS.md - CodeHive Development Guide + +## Project Overview + +**CodeHive** is a collaborative educational platform that allows teachers to create groups with students who can deliver code by editing it directly from the platform. This is a full-stack monorepo with three main components: + +- **codehive-backend**: Spring Boot 3.5.6 REST API (Java 21) +- **codehive-frontend**: React Router v7 SPA (TypeScript, React 19) +- **codehive-worker**: Spring Boot 4.0.1 worker service for sandboxed code execution (Java 21) + +Current development focuses on authentication, user management, and the foundation for code execution infrastructure with RabbitMQ integration. + +--- + +## Repository Structure + +``` +CodeHive/ +├── codehive-backend/ # Main Spring Boot API +│ ├── src/main/java/com/github/codehive/ +│ │ ├── config/ # Spring configuration (Security, OpenAPI, Cache, etc.) +│ │ ├── controller/ # REST controllers (Auth, RecoveryPassword) +│ │ ├── model/ +│ │ │ ├── entity/ # JPA entities (User, PasswordResetToken) +│ │ │ ├── dto/ # Data Transfer Objects (UserDTO) +│ │ │ ├── request/ # Request POJOs organized by domain (auth/, recovery/) +│ │ │ ├── response/ # Response POJOs with inheritance hierarchy +│ │ │ ├── exception/ # Custom exceptions organized by domain +│ │ │ ├── enums/ # Enums (Role, Scope) +│ │ │ └── mapper/ # Entity <-> DTO mappers +│ │ ├── repository/ # JPA repositories +│ │ ├── service/ # Business logic layer +│ │ ├── security/ # JWT filter, UserDetailsService implementation +│ │ ├── ratelimit/ # Bucket4j-based rate limiting with AOP +│ │ └── utils/ # Utility classes (JwtUtil) +│ ├── src/test/java/ # Unit and integration tests (121 tests, 70%+ coverage) +│ ├── build.gradle.kts # Gradle build configuration +│ ├── docker-compose.yaml # PostgreSQL + RabbitMQ containers +│ └── .env # Environment variables (DB, JWT, Mail) +│ +├── codehive-worker/ # Code execution worker service +│ ├── src/main/java/com/github/codehive/worker/ +│ ├── build.gradle.kts # Spring Boot 4.0.1, RabbitMQ, Docker Java client +│ └── settings.gradle.kts +│ +├── codehive-frontend/ # React Router v7 frontend +│ ├── app/ +│ │ ├── routes/ # Page routes (home, login, signup, etc.) +│ │ ├── components/ # Reusable UI components +│ │ ├── services/ # API service classes (AuthService, RecoveryPasswordService) +│ │ ├── types/ # TypeScript types (request, response, model) +│ │ ├── context/ # React contexts (ThemeContext) +│ │ └── pages/ # Page components +│ ├── package.json # npm scripts and dependencies +│ └── vite.config.ts # Vite + React Router + Tailwind CSS +│ +├── .github/workflows/ # CI/CD pipelines +│ └── backend-ci.yml # Automated testing, coverage, build +└── PR_template.md # Pull request template +``` + +--- + +## Essential Commands + +### Backend (codehive-backend) + +**Prerequisites:** +- Java 21 +- Docker & Docker Compose (for PostgreSQL + RabbitMQ) + +**Development:** +```bash +cd codehive-backend + +# Start infrastructure (PostgreSQL + RabbitMQ) +docker-compose up -d + +# Run the application (port 8080) +./gradlew bootRun + +# Build +./gradlew build + +# Build without tests +./gradlew build -x test +``` + +**Testing:** +```bash +# Run all tests (121 tests: 71 unit + 50 integration) +./gradlew test + +# Run unit tests only +./gradlew test --tests "*Test" --exclude-tests "*IntegrationTest" + +# Run integration tests only +./gradlew test --tests "*IntegrationTest" + +# Generate coverage report (target: 70%+) +./gradlew test jacocoTestReport + +# View coverage report +open build/reports/jacoco/test/html/index.html +``` + +**Database:** +```bash +# Docker containers defined in docker-compose.yaml +docker-compose up -d postgres # PostgreSQL 18.1-alpine (port 5432) +docker-compose up -d rabbitmq # RabbitMQ 4.2.2-management (ports 5672, 15672) +docker-compose down +``` + +**API Documentation:** +- Swagger UI: http://localhost:8080/swagger-ui.html +- OpenAPI JSON: http://localhost:8080/v3/api-docs + +### Worker (codehive-worker) + +**Development:** +```bash +cd codehive-worker + +# Build +./gradlew build + +# Run +./gradlew bootRun + +# Test +./gradlew test +``` + +### Frontend (codehive-frontend) + +**Prerequisites:** +- Node.js 18+ + +**Development:** +```bash +cd codehive-frontend + +# Install dependencies +npm install + +# Start dev server (port 3000 or 5173) +npm run dev + +# Build for production +npm run build + +# Start production server +npm run start + +# Type checking +npm run typecheck +``` + +**Environment Variables:** +- Set `VITE_API_URL` in `.env` (defaults to http://localhost:8080) + +--- + +## Code Patterns & Conventions + +### Backend Java Patterns + +#### Package Organization +- **Domain-based subpackages**: `model/request/auth/`, `model/exception/recovery/` +- **Consistent naming**: `{Action}Request`, `{Domain}Service`, `{Entity}Repository` +- **Clear separation**: Controllers → Services → Repositories → Entities + +#### Request/Response Objects + +**Requests** (`model/request/{domain}/`): +```java +// Pattern: {Action}Request in domain subfolder +// Example: model/request/auth/LoginRequest.java +public class LoginRequest { + @NotBlank(message = "Email is required") + private String identifier; + + @Size(min = 6, message = "Password must be at least 6 characters") + private String password; + + // No-arg constructor + parameterized constructor + // Standard getters/setters +} +``` + +**Responses** (`model/response/`): +- **Base class**: `ApiResponse` (abstract, has `success` + `message`) +- **Generic wrapper**: `SuccessResponse` with `data` field +- **Domain-specific**: `AuthResponse` in `response/auth/` subfolder +- **All use**: `@JsonInclude(JsonInclude.Include.NON_NULL)` + +```java +// Example: model/response/SuccessResponse.java +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SuccessResponse extends ApiResponse { + private final T data; + + public SuccessResponse(String message, T data) { + super(true, message); + this.data = data; + } +} +``` + +#### Custom Exceptions + +**Pattern**: Domain-organized exceptions extending `RuntimeException` +```java +// Location: model/exception/{domain}/{Description}Exception.java +// Example: model/exception/auth/IncorrectCredentialsException.java +public class IncorrectCredentialsException extends RuntimeException { + // Always include 4 constructors: + public IncorrectCredentialsException(String message) { super(message); } + public IncorrectCredentialsException(String message, Throwable cause) { super(message, cause); } + public IncorrectCredentialsException(Throwable cause) { super(cause); } + public IncorrectCredentialsException() { super("The provided credentials are incorrect."); } +} +``` + +**Exception Handling**: `GlobalExceptionHandler` with `@RestControllerAdvice` +- **401**: Auth failures (`IncorrectCredentialsException`, `InvalidJWTException`) +- **404**: Not found (`EntityNotFoundException`, `TokenNotFoundException`) +- **409**: Conflicts (`AlreadyRegisteredEmailException`) +- **400**: Validation errors (`MethodArgumentNotValidException`) +- **429**: Rate limit exceeded (`RateLimitExceededException`) +- **500**: Generic runtime exceptions + +#### Controllers + +**Pattern**: REST controllers with OpenAPI annotations + rate limiting +```java +@RestController +@RequestMapping("/api/auth") +@Tag(name = "Authentication", description = "Authentication management APIs") +public class AuthController { + private final AuthService authService; + + // Constructor injection (no @Autowired needed) + public AuthController(AuthService authService) { + this.authService = authService; + } + + @Operation(summary = "User login", description = "Authenticate user...") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Login successful", + content = @Content(schema = @Schema(implementation = SuccessResponse.class))), + @ApiResponse(responseCode = "401", description = "Invalid credentials", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @RateLimit(limit = 5, duration = 60, message = "Too many login attempts. Please try again in 1 minute.") + @PostMapping("/login") + public ResponseEntity> login(@Valid @RequestBody LoginRequest request) { + AuthResponse authResponse = authService.login(request); + return ResponseEntity.ok(new SuccessResponse<>("Login successful", authResponse)); + } +} +``` + +#### Services + +**Pattern**: Business logic with `@Service` and `@Transactional` +```java +@Service +public class AuthService { + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final JwtUtil jwtUtil; + + // Constructor injection + public AuthService(UserRepository userRepository, PasswordEncoder passwordEncoder, JwtUtil jwtUtil) { + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + this.jwtUtil = jwtUtil; + } + + @Transactional(readOnly = true) + public AuthResponse login(LoginRequest request) { + // Business logic + // Throw custom exceptions on error + // Return DTOs or response objects + } +} +``` + +#### Repositories + +**Pattern**: Spring Data JPA interfaces +```java +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); + Optional findByEnrollmentNumber(String enrollmentNumber); +} +``` + +#### Rate Limiting + +**Implementation**: Annotation-based AOP with Bucket4j + +1. **Annotation** (`ratelimit/RateLimit.java`): +```java +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface RateLimit { + int limit(); // Max requests + int duration(); // Time window in seconds + String message(); // Error message +} +``` + +2. **Usage on controllers**: +```java +@RateLimit(limit = 5, duration = 60, message = "Too many login attempts...") +@PostMapping("/login") +``` + +3. **Aspect intercepts** and checks `RateLimitService` (IP-based buckets) + +#### Testing Conventions + +**Unit Tests** (`*Test.java`): +- `@ExtendWith(MockitoExtension.class)` +- `@Mock` for dependencies, `@InjectMocks` for service under test +- Nested test classes with `@Nested` and `@DisplayName` +- AssertJ assertions: `assertThat(...).isEqualTo(...)`, `assertThatThrownBy(...)` + +**Integration Tests** (`*IntegrationTest.java`): +- `@SpringBootTest` + `@AutoConfigureMockMvc` + `@Transactional` +- `@ActiveProfiles("test")` (uses H2 in-memory database) +- `MockMvc` for HTTP testing +- Real database interactions with test data setup in `@BeforeEach` + +**Test Structure**: +```java +@ExtendWith(MockitoExtension.class) +@DisplayName("AuthService Unit") +class AuthServiceTest { + @Mock private UserRepository userRepository; + @InjectMocks private AuthService authService; + + @Nested + @DisplayName("login()") + class LoginTests { + @Test + @DisplayName("Returns AuthResponse for valid credentials") + void login_WithValidCredentials_ReturnsAuthResponse() { + // Given + // When + // Then + } + } +} +``` + +### Frontend TypeScript/React Patterns + +#### Routing +- **React Router v7** with file-based routing in `app/routes/` +- Routes defined in `app/routes.ts` +- Pattern: `route("path", "routes/filename.tsx")` + +#### Service Layer +```typescript +// Pattern: {Domain}Service class with methods returning Promises +// Example: services/AuthService.ts +class AuthServiceClass { + private readonly baseUrl = `${API_BASE_URL}/api/auth`; + + async login(credentials: LoginRequest): Promise> { + const response = await fetch(`${this.baseUrl}/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(credentials), + }); + + const data = await response.json(); + if (!response.ok) { + throw new Error(data.message || "Login failed"); + } + return data; + } + + // Token management methods + setToken(token: string): void { localStorage.setItem("authToken", token); } + getToken(): string | null { return localStorage.getItem("authToken"); } + removeToken(): void { localStorage.removeItem("authToken"); } +} + +export const AuthService = new AuthServiceClass(); +``` + +#### Type Definitions +- **Request types**: `types/request/Auth.ts` +- **Response types**: `types/response/Api.ts` +- **Model types**: `types/model/User.ts` +- Mirror backend DTOs exactly + +#### Component Organization +- **Pages**: `pages/{PageName}Page.tsx` (composed of components) +- **Components**: `components/{ComponentName}.tsx` (reusable) +- **Routes**: `routes/{route-name}.tsx` (route handlers that render pages) + +#### Styling +- **Tailwind CSS v4** via `@tailwindcss/vite` +- Dark mode support via `ThemeContext` +- Utility-first classes + +--- + +## Security & Authentication + +### JWT Authentication Flow + +1. **Login/Signup**: User submits credentials → Backend validates → Returns JWT token + user data +2. **Token Storage**: Frontend stores token in `localStorage` via `AuthService.setToken()` +3. **Protected Requests**: Frontend sends `Authorization: Bearer ` header +4. **Backend Validation**: `JWTAuthenticationFilter` intercepts, validates token, sets `SecurityContextHolder` + +### JWT Configuration +- **Secret**: Stored in `.env` as `JWT_SECRET` (base64-encoded, min 256 bits) +- **Expiration**: `JWT_EXPIRATION_MS` (default: 86400000ms = 24 hours) +- **Claims**: `sub` (user email), `scope` (user scopes), `iat`/`exp` timestamps + +### Security Configuration +- **CSRF**: Disabled (stateless JWT authentication) +- **CORS**: Configured for `FRONTEND_URL` from `.env` +- **Sessions**: Stateless (`.sessionManagement().sessionCreationPolicy(STATELESS)`) +- **Password Encoding**: BCrypt with strength 10 + +### Rate Limiting +- **Login**: 5 requests per 60 seconds per IP +- **Signup**: 3 requests per 5 minutes per IP +- **Forgot Password**: 3 requests per 5 minutes per IP +- **Reset Password**: 5 requests per 5 minutes per IP + +--- + +## Environment Variables + +### Backend `.env` (codehive-backend/) +```bash +# Database +DATABASE_NAME=codehive +DATABASE_USERNAME=myuser +DATABASE_PASSWORD=secret + +# JWT +JWT_SECRET= +JWT_EXPIRATION_MS=86400000 + +# Mail (SMTP) +MAIL_HOST=smtp.gmail.com +MAIL_PORT=587 +MAIL_USERNAME=your-email@gmail.com +MAIL_PASSWORD=your-app-password +MAIL_SMTP_AUTH=true +MAIL_STARTTLS_ENABLE=true + +# Frontend URL (for CORS) +FRONTEND_URL=http://localhost:5173 +``` + +### Frontend `.env` (codehive-frontend/) +```bash +VITE_API_URL=http://localhost:8080 +``` + +--- + +## Database + +### Technology +- **Production**: PostgreSQL 18.1-alpine +- **Testing**: H2 in-memory database (configured in `application-test.properties`) + +### Entities + +**User** (`model/entity/User.java`): +- `id` (Long, auto-generated) +- `name`, `lastName`, `enrollmentNumber` (unique), `email` (unique) +- `password` (BCrypt-encoded) +- `role` (enum: STUDENT, TEACHER, ADMIN) +- `scopes` (List, many-to-many in `user_scopes` table) +- `profilePictureUrl`, `isActive`, `createdAt` +- Implements `UserDetails` for Spring Security + +**PasswordResetToken** (`model/entity/PasswordResetToken.java`): +- `id`, `token` (UUID), `user` (ManyToOne), `expiryDate`, `used` + +### Migration Strategy +- Currently: JPA auto-DDL (Hibernate `ddl-auto=update`) +- **TODO**: Migrate to Flyway/Liquibase for production + +--- + +## API Endpoints + +### Authentication (`/api/auth`) + +#### `POST /api/auth/login` +**Rate Limit**: 5 req/60s +**Request**: +```json +{ + "identifier": "user@example.com or ENR001", + "password": "password123" +} +``` +**Response** (200): +```json +{ + "success": true, + "message": "Login successful", + "data": { + "token": "eyJhbGciOiJIUzI1...", + "user": { + "id": 1, + "name": "John", + "lastName": "Doe", + "email": "user@example.com", + "enrollmentNumber": "ENR001", + "role": "STUDENT", + "scopes": ["READ", "WRITE"], + "profilePictureUrl": "...", + "isActive": true, + "createdAt": "2024-01-15T10:00:00" + } + } +} +``` + +#### `POST /api/auth/signup` +**Rate Limit**: 3 req/5min +**Request**: +```json +{ + "name": "Jane", + "lastName": "Doe", + "email": "jane@example.com", + "enrollmentNumber": "ENR002", + "password": "password123", + "role": "STUDENT" +} +``` +**Response** (201): Same as login response + +### Password Recovery (`/api/recovery-password`) + +#### `POST /api/recovery-password/forgot` +**Rate Limit**: 3 req/5min +**Request**: +```json +{ + "email": "user@example.com" +} +``` +**Response** (200): +```json +{ + "success": true, + "message": "Password reset email sent" +} +``` + +#### `POST /api/recovery-password/reset` +**Rate Limit**: 5 req/5min +**Request**: +```json +{ + "token": "uuid-token-from-email", + "newPassword": "newpassword123" +} +``` +**Response** (200): +```json +{ + "success": true, + "message": "Password reset successful" +} +``` + +--- + +## CI/CD + +### GitHub Actions Workflow (`.github/workflows/backend-ci.yml`) + +**Triggers**: +- Push to `main` or `develop` branches (backend changes only) +- Pull requests to `main` or `develop` + +**Pipeline Steps**: +1. **Setup**: Java 21 (Temurin), Gradle caching +2. **Test**: `./gradlew test --info` with `test` profile (H2 database) +3. **Coverage**: `./gradlew jacocoTestReport` +4. **Build**: `./gradlew build -x test` (creates JAR) +5. **Artifacts**: Upload test results, coverage reports, JAR +6. **PR Checks**: Publish test results, add coverage comment (min 70% overall, 80% changed files) + +**Requirements for Passing**: +- All tests pass (121+ tests) +- No build errors +- Coverage thresholds met + +--- + +## Testing Strategy + +### Test Pyramid +- **70% Unit Tests**: Fast, isolated, mock dependencies +- **30% Integration Tests**: Slower, full Spring context, real database + +### Coverage Targets +- **Overall**: 70%+ (enforced in CI) +- **Changed Files**: 80%+ (enforced in PR checks) +- **Current**: 70%+ (121 tests) + +### Writing Tests + +**For new features, always add**: +1. **Unit tests** for service layer logic +2. **Integration tests** for controller endpoints +3. **Edge cases**: null inputs, invalid data, error paths +4. **Security tests**: unauthorized access, rate limiting + +**Test Naming**: +- Method: `methodName_Scenario_ExpectedOutcome` +- Display: `@DisplayName("Human-readable description")` + +**Example**: +```java +@Test +@DisplayName("Throws IncorrectCredentialsException when password is wrong") +void login_WithWrongPassword_ThrowsIncorrectCredentialsException() { + // Given + LoginRequest request = new LoginRequest("user@example.com", "wrongpass"); + when(userRepository.findByEmail("user@example.com")).thenReturn(Optional.of(testUser)); + when(passwordEncoder.matches("wrongpass", testUser.getPassword())).thenReturn(false); + + // When/Then + assertThatThrownBy(() -> authService.login(request)) + .isInstanceOf(IncorrectCredentialsException.class) + .hasMessage("Invalid credentials"); +} +``` + +--- + +## Git Workflow + +### Branch Naming +- **Feature**: `feature/{description}` or `feature/{jira-task-id}` +- **Bugfix**: `bugfix/{description}` +- **Hotfix**: `hotfix/{description}` + +### Commit Messages +- Use conventional commits (optional but recommended) +- Examples: `feat: add user login`, `fix: resolve JWT expiration bug`, `test: add integration tests for signup` + +### Pull Requests +- Use `PR_template.md` checklist +- **Required checks**: + - [ ] All tests pass + - [ ] Coverage thresholds met + - [ ] Code follows style guidelines + - [ ] Self-review completed + - [ ] Documentation updated (if needed) + +### Current Branch +- Active development on `feature/rabbitMQ` (integrating RabbitMQ for worker communication) + +--- + +## Common Tasks + +### Adding a New API Endpoint + +1. **Define Request DTO** in `model/request/{domain}/{Action}Request.java`: + - Add Jakarta validation annotations + - No-arg + parameterized constructors + - Getters/setters + +2. **Define Response DTO** (if needed) in `model/response/{domain}/{Action}Response.java`: + - Extend `ApiResponse` or use `SuccessResponse` + - Add `@JsonInclude(JsonInclude.Include.NON_NULL)` + +3. **Create Custom Exceptions** in `model/exception/{domain}/{Error}Exception.java`: + - Extend `RuntimeException` + - Add 4 constructors + +4. **Update GlobalExceptionHandler** in `model/exception/handler/GlobalExceptionHandler.java`: + - Add `@ExceptionHandler` method for new exception + - Return `ResponseEntity` with appropriate HTTP status + +5. **Implement Service Logic** in `service/{Domain}Service.java`: + - `@Service` annotation + - Constructor injection of dependencies + - `@Transactional` on methods + - Throw custom exceptions on errors + +6. **Create Controller Endpoint** in `controller/{Domain}Controller.java`: + - `@RestController` + `@RequestMapping("/api/{domain}")` + - OpenAPI annotations (`@Operation`, `@ApiResponses`) + - `@RateLimit` if needed + - Call service method, return `ResponseEntity>` + +7. **Write Tests**: + - Unit tests for service (`{Domain}ServiceTest.java`) + - Integration tests for controller (`{Domain}ControllerIntegrationTest.java`) + - Aim for 70%+ coverage + +8. **Update API Documentation**: + - OpenAPI annotations auto-generate Swagger docs + - Test at http://localhost:8080/swagger-ui.html + +### Adding a New Entity + +1. **Create Entity** in `model/entity/{Entity}.java`: + - `@Entity` + `@Table(name = "...")` + - JPA annotations on fields + - No-arg constructor + builder pattern (optional) + +2. **Create Repository** in `repository/{Entity}Repository.java`: + - Extend `JpaRepository` + - Add custom query methods (Spring Data JPA naming) + +3. **Create DTO** in `model/dto/{Entity}DTO.java`: + - Fields for external representation + - `@JsonInclude(JsonInclude.Include.NON_NULL)` + +4. **Create Mapper** in `model/mapper/{Entity}Mapper.java`: + - Static methods: `toDTO(Entity)`, `toEntity(DTO)` + +5. **Update Database**: + - Currently auto-DDL, no manual migration needed + - For production, add Flyway/Liquibase migration + +### Adding Frontend Page + +1. **Create Route Handler** in `app/routes/{route-name}.tsx`: + - Import page component + - Export default component function + +2. **Register Route** in `app/routes.ts`: + - Add `route("path", "routes/{route-name}.tsx")` + +3. **Create Page Component** in `app/pages/{PageName}Page.tsx`: + - Use existing components from `app/components/` + - Call service methods from `app/services/` + +4. **Define Types** (if needed): + - Request types in `app/types/request/` + - Response types in `app/types/response/` + - Model types in `app/types/model/` + +5. **Add Service Method** in `app/services/{Domain}Service.ts`: + - Follow existing pattern with `fetch` API + - Handle errors with try/catch + - Return typed promises + +--- + +## Gotchas & Important Notes + +### Backend + +1. **Identifier in Login**: The `LoginRequest.identifier` field accepts **both** email and enrollment number. `AuthService.isEmail()` determines which to use. + +2. **Rate Limiting by IP**: Rate limits are enforced per IP address (extracted from `X-Forwarded-For` or `RemoteAddr`). In development behind a proxy, all requests may share the same IP. + +3. **Password Encoding**: Always use `passwordEncoder.encode()` before saving passwords. Never store plain text. + +4. **Transaction Management**: Use `@Transactional` on service methods that modify data. Use `readOnly = true` for read-only operations (performance optimization). + +5. **H2 Console in Tests**: Test profile uses H2 in-memory database. Data is reset between tests due to `@Transactional` on test classes. + +6. **JWT Secret**: Must be at least 256 bits (32 bytes) base64-encoded. Generate with: `openssl rand -base64 32` + +7. **Gradle Wrapper**: Always use `./gradlew` (Unix) or `gradlew.bat` (Windows), not system-installed Gradle, to ensure correct version. + +8. **Docker Compose Integration**: Spring Boot DevTools auto-detects `docker-compose.yaml` and starts containers. Can be disabled by removing `spring-boot-docker-compose` dependency. + +9. **Scope System**: Currently defined but not fully implemented. `User.scopes` is a placeholder for future fine-grained permissions. + +10. **OpenAPI Documentation**: Always add `@Operation` and `@ApiResponses` to controller methods. Swagger UI auto-updates on application restart. + +### Frontend + +1. **API URL Configuration**: Set `VITE_API_URL` in `.env` for non-localhost backends. Defaults to `http://localhost:8080`. + +2. **React Router v7**: Uses file-based routing. Routes must be registered in `routes.ts`, not auto-discovered. + +3. **SSR Enabled**: Server-side rendering is enabled by default (`ssr: true` in `react-router.config.ts`). Can be disabled for pure SPA mode. + +4. **Token Expiration**: Frontend doesn't automatically handle token expiration. Backend returns 401, but frontend needs to implement redirect to login. + +5. **Dark Mode**: Managed by `ThemeContext`. Use `useTheme()` hook to access `theme` and `toggleTheme()`. + +6. **Tailwind CSS v4**: Latest version with different configuration. Check official docs if migrating from v3. + +### Testing + +1. **Test Isolation**: Integration tests use `@Transactional` to rollback after each test. Don't rely on database state between tests. + +2. **Mocking vs Real Beans**: Unit tests mock dependencies. Integration tests use real Spring beans (except external services like SMTP). + +3. **Coverage Reports**: Generated in `build/reports/jacoco/test/html/index.html` after `./gradlew jacocoTestReport`. + +4. **Flaky Tests**: Rate limiting tests may be flaky if system clock skews. Use `@DirtiesContext` if needed. + +### Infrastructure + +1. **RabbitMQ Integration**: Currently in development on `feature/rabbitMQ` branch. Worker service will consume code execution jobs from RabbitMQ queue. + +2. **Docker Networking**: Backend, worker, PostgreSQL, and RabbitMQ communicate via Docker Compose network. Check `docker-compose.yaml` for service names. + +3. **Port Conflicts**: Ensure ports 5432 (PostgreSQL), 5672/15672 (RabbitMQ), 8080 (backend), 3000/5173 (frontend) are available. + +--- + +## Troubleshooting + +### Backend Won't Start + +**Issue**: `Connection refused` to PostgreSQL +**Solution**: Ensure Docker containers are running: `docker-compose up -d` + +**Issue**: `Invalid JWT_SECRET` error +**Solution**: Check `.env` has valid base64-encoded secret (min 256 bits) + +**Issue**: Tests fail with database errors +**Solution**: Check `application-test.properties` is configured for H2, not PostgreSQL + +### Frontend Can't Connect to Backend + +**Issue**: CORS errors in browser console +**Solution**: Check `FRONTEND_URL` in backend `.env` matches frontend dev server URL + +**Issue**: 404 on API calls +**Solution**: Verify `VITE_API_URL` in frontend `.env` is correct + +### Tests Failing + +**Issue**: Rate limit tests fail intermittently +**Solution**: Add `Thread.sleep()` between requests or use `@DirtiesContext` + +**Issue**: Integration tests fail with "table not found" +**Solution**: Ensure `@ActiveProfiles("test")` is present on test class + +### Coverage Below Threshold + +**Issue**: CI fails with "Coverage below 70%" +**Solution**: Add more unit tests for uncovered service methods. Focus on edge cases and error paths. + +--- + +## Additional Resources + +- **Spring Boot Docs**: https://spring.io/projects/spring-boot +- **React Router v7 Docs**: https://reactrouter.com/ +- **Bucket4j (Rate Limiting)**: https://bucket4j.com/ +- **Jacoco (Coverage)**: https://www.jacoco.org/jacoco/ + +--- + +## Future Enhancements + +1. **Code Execution**: Complete worker service integration with sandboxed Docker containers +2. **Real-time Collaboration**: WebSocket support for live code editing +3. **Group Management**: Teacher/student group creation and assignment submission +4. **File Upload**: Support for uploading code files and project archives +5. **Flyway Migrations**: Replace JPA auto-DDL with versioned migrations +6. **Refresh Tokens**: Implement refresh token rotation for longer sessions +7. **Email Templates**: HTML email templates for password reset and notifications +8. **Admin Dashboard**: Frontend admin panel for user management +9. **API Versioning**: Add `/v1/` prefix to API routes for future compatibility +10. **Monitoring**: Add Actuator endpoints and Prometheus metrics + +--- + +**Last Updated**: 2025-01-15 +**Current Branch**: `feature/rabbitMQ` +**Project Status**: Active Development (Alpha) diff --git a/codehive-backend/docker-compose.yaml b/codehive-backend/docker-compose.yaml index d730bad..cf6389c 100644 --- a/codehive-backend/docker-compose.yaml +++ b/codehive-backend/docker-compose.yaml @@ -11,5 +11,12 @@ services: volumes: - 'db_data:/var/lib/postgresql/data' + rabbitmq: + image: 'rabbitmq:4.2.2-management-alpine' + container_name: 'rabbitmq' + ports: + - '5672:5672' + - '15672:15672' + volumes: db_data: diff --git a/codehive-worker/.gitattributes b/codehive-worker/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/codehive-worker/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/codehive-worker/.gitignore b/codehive-worker/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/codehive-worker/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/codehive-worker/build.gradle.kts b/codehive-worker/build.gradle.kts new file mode 100644 index 0000000..2cdeaed --- /dev/null +++ b/codehive-worker/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + java + id("org.springframework.boot") version "4.0.1" + id("io.spring.dependency-management") version "1.1.7" +} + +group = "com.github.codehive" +version = "0.0.1-SNAPSHOT" +description = "Worker service for sandboxed code execution" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter") + implementation("org.springframework.boot:spring-boot-starter-amqp") + implementation("com.github.docker-java:docker-java-core:3.7.0") + implementation("com.github.docker-java:docker-java-transport-httpclient5:3.7.0") + testImplementation("org.springframework.boot:spring-boot-starter-test") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + + // Timeout utility + implementation("io.github.resilience4j:resilience4j-timelimiter:2.3.0") +} + +tasks.withType { + useJUnitPlatform() +} diff --git a/codehive-worker/gradle/wrapper/gradle-wrapper.jar b/codehive-worker/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f8e1ee3125fe0768e9a76ee977ac089eb657005e GIT binary patch literal 45633 zcma&NV|1n6wyqu9PQ|uu+csuwn-$x(T~Woh?Nr6KUD3(A)@l1Yd+oj6Z_U=8`RAE` z#vE6_`?!1WLs1443=Ieh3JM4ai0JG2|2{}S&_HrxszP*9^5P7#QX*pVDq?D?;6T8C z{bWO1$9at%!*8ax*TT&F99vwf1Ls+3lklsb|bC`H`~Q z_w}*E9P=Wq;PYlGYhZ^lt#N97bt5aZ#mQcOr~h^B;R>f-b0gf{y(;VA{noAt`RZzU z7vQWD{%|q!urW2j0Z&%ChtL(^9m` zgaU%|B;V#N_?%iPvu0PVkX=1m9=*SEGt-Lp#&Jh%rz6EJXlV^O5B5YfM5j{PCeElx z8sipzw8d=wVhFK+@mgrWyA)Sv3BJq=+q+cL@=wuH$2;LjY z^{&+X4*HFA0{QvlM_V4PTQjIdd;d|2YuN;s|bi!@<)r-G%TuOCHz$O(_-K z)5in&6uNN<0UfwY=K>d;cL{{WK2FR|NihJMN0Q4X+(1lE)$kY?T$7UWleIU`i zQG#X-&&m-8x^(;n@o}$@vPMYRoq~|FqC~CU3MnoiifD{(CwAGd%X#kFHq#4~%_a!{ zeX{XXDT#(DvX7NtAs7S}2ZuiZ>gtd;tCR7E)3{J^`~#Vd**9qz%~JRFAiZf{zt|Dr zvQw!)n7fNUn_gH`o9?8W8t_%x6~=y*`r46bjj(t{YU*qfqd}J}*mkgUfsXTI>Uxl6 z)Fj>#RMy{`wINIR;{_-!xGLgVaTfNJ2-)%YUfO&X5z&3^E#4?k-_|Yv$`fpgYkvnA%E{CiV zP|-zAf8+1@R`sT{rSE#)-nuU7Pwr-z>0_+CLQT|3vc-R22ExKT4ym@Gj77j$aTVns zp4Kri#Ml?t7*n(;>nkxKdhOU9Qbwz%*#i9_%K<`m4T{3aPbQ?J(Mo`6E5cDdbAk%X z+4bN%E#a(&ZXe{G#V!2Nt+^L$msKVHP z|APpBhq7knz(O2yY)$$VyI_Xg4UIC*$!i7qQG~KEZnO@Q1i89@4ZKW*3^Wh?o?zSkfPxdhnTxlO!3tAqe_ zuEqHVcAk3uQIFTpP~C{d$?>7yt3G3Fo>syXTus>o0tJdFpQWC27hDiwC%O09i|xCq z@H6l|+maB;%CYQIChyhu;PVYz9e&5a@EEQs3$DS6dLIS+;N@I0)V}%B`jdYv;JDck zd|xxp(I?aedivE7*19hesoa-@Xm$^EHbbVmh$2^W-&aTejsyc$i+}A#n2W*&0Qt`5 zJS!2A|LVV;L!(*x2N)GjJC;b1RB_f(#D&g_-};a*|BTRvfdIX}Gau<;uCylMNC;UG zzL((>6KQBQ01wr%7u9qI2HLEDY!>XisIKb#6=F?pAz)!_JX}w|>1V>X^QkMdFi@Jr z`1N*V4xUl{qvECHoF?#lXuO#Dg2#gh|AU$Wc=nuIbmVPBEGd(R#&Z`TP9*o%?%#ob zWN%ByU+55yBNfjMjkJnBjT!cVDi}+PR3N&H(f8$d^Pu;A_WV*{)c2Q{IiE7&LPsd4 z!rvkUf{sco_WNSIdW+btM#O+4n`JiceH6%`7pDV zRqJ@lj=Dt(e-Gkz$b!c2>b)H$lf(fuAPdIsLSe(dZ4E~9+Ge!{3j~>nS%r)eQZ;Iq ztWGpp=2Ptc!LK_TQ8cgJXUlU5mRu|7F2{eu*;a>_5S<;bus=t*IXcfzJRPv4xIs;s zt2<&}OM>KxkTxa=dFMfNr42=DL~I}6+_{`HT_YJBiWkpVZND1Diad~Yr*Fuq{zljr z*_+jXk=qVBdwlQkYuIrB4GG*#voba$?h*u0uRNL+87-?AjzG2X_R9mzQ7BJEawutObr|ey~%in>6k%A`K*`pb-|DF5m})!`b=~osoiW2)IFh?_y9y<3Cix_ znvC=bjBX1J820!%%9FaB@v?hAsd05e@w$^ZAvtUp*=Bi+Owkl?rLa6F#yl{s+?563 zmn2 zV95%gySAJ$L!Vvk4kx!n@mo`3Mfi`2lXUkBmd%)u)7C?Pa;oK~zUQ#p0u{a|&0;zNO#9a4`v^3df90X#~l_k$q7n&L5 z?TszF842~g+}tgUP}UG?ObLCE1(Js_$e>XS7m%o7j@@VdxePtg)w{i5an+xK95r?s zDeEhgMO-2$H?@0{p-!4NJ)}zP+3LzZB?FVap)ObHV6wp}Lrxvz$cjBND1T6ln$EfJ zZRPeR2lP}K0p8x`ahxB??Ud;i7$Y5X!5}qBFS+Zp=P^#)08nQi_HuJcN$0=x;2s53 zwoH}He9BlKT4GdWfWt)@o@$4zN$B@5gVIN~aHtwIhh{O$uHiMgYl=&Vd$w#B2 zRv+xK3>4E{!)+LXA2#*K6H~HpovXAQeXV(^Pd%G_>ro0(4_@`{2Ag(+8{9pqJ>Co$ zRRV(oX;nD+Jel_2^BlNO=cQP8q*G#~R3PTERUxvug_C4T3qwb9MQE|^{5(H*nt`fn z^%*p-RwkAhT6(r>E@5w8FaB)Q<{#`H9fTdc6QBuSr9D-x!Tb9f?wI=M{^$cB5@1;0 z+yLHh?3^c-Qte@JI<SW`$bs5Vv9!yWjJD%oY z8Cdc$a(LLy@tB2)+rUCt&0$&+;&?f~W6+3Xk3g zy9L�|d9Zj^A1Dgv5yzCONAB>8LM`TRL&7v_NKg(bEl#y&Z$py}mu<4DrT@8HHjE zqD@4|aM>vt!Yvc2;9Y#V;KJ8M>vPjiS2ycq52qkxInUK*QqA3$&OJ`jZBo zpzw&PT%w0$D94KD%}VN9c)eCueh1^)utGt2OQ+DP(BXszodfc1kFPWl~BQ5Psy*d`UIf zc}zQ8TVw35jdCSc78)MljC-g3$GX2$<0<3MEQXS&i<(ZFClz9WlL}}?%u>S2hhEk_ zyzfm&@Q%YVB-vw3KH|lU#c_)0aeG^;aDG&!bwfOz_9)6gLe;et;h(?*0d-RV0V)1l zzliq#`b9Y*c`0!*6;*mU@&EFSbW>9>L5xUX+unp%@tCW#kLfz)%3vwN{1<-R*g+B_C^W8)>?n%G z<#+`!wU$L&dn)Pz(9DGGI%RlmM2RpeDy9)31OZV$c2T>-Jl&4$6nul&e7){1u-{nP zE$uZs%gyanu+yBcAb+jTYGy(^<;&EzeLeqveN12Lvv)FQFn0o&*qAaH+gLJ)*xT9y z>`Y`W?M#K7%w26w?Oen>j7=R}EbZ;+jcowV&i}P|IfW^C5GJHt5D;Q~)|=gW3iQ;N zQGl4SQFtz=&~BGon6hO@mRnjpmM79ye^LY_L2no{f_M?j80pr`o3BrI7ice#8#Zt4 zO45G97Hpef+AUEU%jN-dLmPYHY(|t#D)9|IeB^i1X|eEq+ymld_Uj$l^zVAPRilx- z^II$sL4G~{^7?sik2BK7;ZV-VIVhrKjUxBIsf^N&K`)5;PjVg-DTm1Xtw4-tGtElU zJgVTCk4^N4#-kPuX=7p~GMf5Jj5A#>)GX)FIcOqY4lf}Vv2gjrOTuFusB@ERW-&fb zTp=E0E?gXkwzn)AMMY*QCftp%MOL-cbsG{02$0~b?-JD{-nwj58 zBHO1YL~yn~RpnZ6*;XA|MSJeBfX-D?afH*E!2uGjT%k!jtx~OG_jJ`Ln}lMQb7W41 zmTIRd%o$pu;%2}}@2J$x%fg{DZEa-Wxdu6mRP~Ea0zD2+g;Dl*to|%sO-5mUrZ`~C zjJ zUe^**YRgBvlxl<(r0LjxjSQKiTx+E<7$@9VO=RYgL9ldTyKzfqR;Y&gu^ub!fVX7u z3H@;8j#tVgga~EMuXv_#Q8<*uK@R{mGzn92eDYkF1sbxh5!P|M-D)T~Ae*SO`@u$Q z7=5s)HM)w~s2j5{I67cqSn6BLLhCMcn0=OTVE?T7bAmY!T+xZ_N3op~wZ3Oxlm6(a5qB({6KghlvBd9HJ#V6YY_zxbj-zI`%FN|C*Q`DiV z#>?Kk7VbuoE*I9tJaa+}=i7tJnMRn`P+(08 za*0VeuAz!eI7giYTsd26P|d^E2p1f#oF*t{#klPhgaShQ1*J7?#CTD@iDRQIV+Z$@ z>qE^3tR3~MVu=%U%*W(1(waaFG_1i5WE}mvAax;iwZKv^g1g}qXY7lAd;!QQa#5e= z1_8KLHje1@?^|6Wb(A{HQ_krJJP1GgE*|?H0Q$5yPBQJlGi;&Lt<3Qc+W4c}Ih~@* zj8lYvme}hwf@Js%Oj=4BxXm15E}7zS0(dW`7X0|$damJ|gJ6~&qKL>gB_eC7%1&Uh zLtOkf7N0b;B`Qj^9)Bfh-( z0or96!;EwEMnxwp!CphwxxJ+DDdP4y3F0i`zZp-sQ5wxGIHIsZCCQz5>QRetx8gq{ zA33BxQ}8Lpe!_o?^u2s3b!a-$DF$OoL=|9aNa7La{$zI#JTu_tYG{m2ly$k?>Yc); zTA9ckzd+ibu>SE6Rc=Yd&?GA9S5oaQgT~ER-|EwANJIAY74|6 z($#j^GP}EJqi%)^jURCj&i;Zl^-M9{=WE69<*p-cmBIz-400wEewWVEd^21}_@A#^ z2DQMldk_N)6bhFZeo8dDTWD@-IVunEY*nYRON_FYII-1Q@@hzzFe(lTvqm}InfjQ2 zN>>_rUG0Lhaz`s;GRPklV?0 z;~t4S8M)ZBW-ED?#UNbCrsWb=??P># zVc}MW_f80ygG_o~SW+Q6oeIUdFqV2Fzys*7+vxr^ZDeXcZZc;{kqK;(kR-DKL zByDdPnUQgnX^>x?1Tz~^wZ%Flu}ma$Xmgtc7pSmBIH%&H*Tnm=L-{GzCv^UBIrTH5 zaoPO|&G@SB{-N8Xq<+RVaM_{lHo@X-q}`zjeayVZ9)5&u*Y>1!$(wh9Qoe>yWbPgw zt#=gnjCaT_+$}w^*=pgiHD8N$hzqEuY5iVL_!Diw#>NP7mEd?1I@Io+?=$?7cU=yK zdDKk_(h_dB9A?NX+&=%k8g+?-f&`vhAR}&#zP+iG%;s}kq1~c{ac1@tfK4jP65Z&O zXj8Ew>l7c|PMp!cT|&;o+(3+)-|SK&0EVU-0-c&guW?6F$S`=hcKi zpx{Z)UJcyihmN;^E?*;fxjE3kLN4|&X?H&$md+Ege&9en#nUe=m>ep3VW#C?0V=aS zLhL6v)|%$G5AO4x?Jxy8e+?*)YR~<|-qrKO7k7`jlxpl6l5H&!C4sePiVjAT#)b#h zEwhfkpFN9eY%EAqg-h&%N>E0#%`InXY?sHyptcct{roG42Mli5l)sWt66D_nG2ed@ z#4>jF?sor7ME^`pDlPyQ(|?KL9Q88;+$C&3h*UV*B+*g$L<{yT9NG>;C^ZmPbVe(a z09K^qVO2agL`Hy{ISUJ{khPKh@5-)UG|S8Sg%xbJMF)wawbgll3bxk#^WRqmdY7qv zr_bqa3{`}CCbREypKd!>oIh^IUj4yl1I55=^}2mZAAW6z}Kpt3_o1b4__sQ;b zv)1=xHO?gE-1FL}Y$0YdD-N!US;VSH>UXnyKoAS??;T%tya@-u zfFo)@YA&Q#Q^?Mtam19`(PS*DL{PHjEZa(~LV7DNt5yoo1(;KT)?C7%^Mg;F!C)q= z6$>`--hQX4r?!aPEXn;L*bykF1r8JVDZ)x4aykACQy(5~POL;InZPU&s5aZm-w1L< z`crCS5=x>k_88n(*?zn=^w*;0+8>ui2i>t*Kr!4?aA1`yj*GXi#>$h8@#P{S)%8+N zCBeL6%!Ob1YJs5+a*yh{vZ8jH>5qpZhz_>(ph}ozKy9d#>gba1x3}`-s_zi+SqIeR z0NCd7B_Z|Fl+(r$W~l@xbeAPl5{uJ{`chq}Q;y8oUN0sUr4g@1XLZQ31z9h(fE_y( z_iQ(KB39LWd;qwPIzkvNNkL(P(6{Iu{)!#HvBlsbm`g2qy&cTsOsAbwMYOEw8!+75D!>V{9SZ?IP@pR9sFG{T#R*6ez2&BmP8*m^6+H2_ z>%9pg(+R^)*(S21iHjLmdt$fmq6y!B9L!%+;wL5WHc^MZRNjpL9EqbBMaMns2F(@h zN0BEqZ3EWGLjvY&I!8@-WV-o@>biD;nx;D}8DPapQF5ivpHVim8$G%3JrHtvN~U&) zb1;=o*lGfPq#=9Moe$H_UhQPBjzHuYw;&e!iD^U2veY8)!QX_E(X@3hAlPBIc}HoD z*NH1vvCi5xy@NS41F1Q3=Jkfu&G{Syin^RWwWX|JqUIX_`}l;_UIsj&(AFQ)ST*5$ z{G&KmdZcO;jGIoI^+9dsg{#=v5eRuPO41<*Ym!>=zHAXH#=LdeROU-nzj_@T4xr4M zJI+d{Pp_{r=IPWj&?%wfdyo`DG1~|=ef?>=DR@|vTuc)w{LHqNKVz9`Dc{iCOH;@H5T{ zc<$O&s%k_AhP^gCUT=uzrzlEHI3q`Z3em0*qOrPHpfl1v=8Xkp{!f9d2p!4 zL40+eJB4@5IT=JTTawIA=Z%3AFvv=l1A~JX>r6YUMV7GGLTSaIn-PUw| z;9L`a<)`D@Qs(@P(TlafW&-87mcZuwFxo~bpa01_M9;$>;4QYkMQlFPgmWv!eU8Ut zrV2<(`u-@1BTMc$oA*fX;OvklC1T$vQlZWS@&Wl}d!72MiXjOXxmiL8oq;sP{)oBe zS#i5knjf`OfBl}6l;BSHeY31w8c~8G>$sJ9?^^!)Z*Z*Xg zbTbkcbBpgFui(*n32hX~sC7gz{L?nlnOjJBd@ zUC4gd`o&YB4}!T9JGTe9tqo0M!JnEw4KH7WbrmTRsw^Nf z^>RxG?2A33VG3>E?iN|`G6jgr`wCzKo(#+zlOIzp-^E0W0%^a>zO)&f(Gc93WgnJ2p-%H-xhe{MqmO z8Iacz=Qvx$ML>Lhz$O;3wB(UI{yTk1LJHf+KDL2JPQ6#m%^bo>+kTj4-zQ~*YhcqS z2mOX!N!Q$d+KA^P0`EEA^%>c12X(QI-Z}-;2Rr-0CdCUOZ=7QqaxjZPvR%{pzd21HtcUSU>u1nw?)ZCy+ zAaYQGz59lqhNXR4GYONpUwBU+V&<{z+xA}`Q$fajmR86j$@`MeH}@zz*ZFeBV9Ot< ze8BLzuIIDxM&8=dS!1-hxiAB-x-cVmtpN}JcP^`LE#2r9ti-k8>Jnk{?@Gw>-WhL=v+H!*tv*mcNvtwo)-XpMnV#X>U1F z?HM?tn^zY$6#|(|S~|P!BPp6mur58i)tY=Z-9(pM&QIHq+I5?=itn>u1FkXiehCRC zW_3|MNOU)$-zrjKnU~{^@i9V^OvOJMp@(|iNnQ%|iojG2_Snnt`1Cqx2t)`vW&w2l zwb#`XLNY@FsnC-~O&9|#Lpvw7n!$wL9azSk)$O}?ygN@FEY({2%bTl)@F2wevCv`; zZb{`)uMENiwE|mti*q5U4;4puX{VWFJ#QIaa*%IHKyrU*HtjW_=@!3SlL~pqLRs?L zoqi&}JLsaP)yEH!=_)zmV-^xy!*MCtc{n|d%O zRM>N>eMG*Qi_XAxg@82*#zPe+!!f#;xBxS#6T-$ziegN-`dLm z=tTN|xpfCPng06|X^6_1JgN}dM<_;WsuL9lu#zLVt!0{%%D9*$nT2E>5@F(>Fxi%Y zpLHE%4LZSJ1=_qm0;^Wi%x56}k3h2Atro;!Ey}#g&*BpbNXXS}v>|nn=Mi0O(5?=1V7y1^1Bdt5h3}oL@VsG>NAH z1;5?|Sth=0*>dbXSQ%MQKB?eN$LRu?yBy@qQVaUl*f#p+sLy$Jd>*q;(l>brvNUbIF0OCf zk%Q;Zg!#0w0_#l)!t?3iz~`X8A>Yd3!P&A4Ov6&EdZmOixeTd4J`*Wutura(}4w@KV>i#rf(0PYL&v^89QiXBP6sj=N;q8kVxS}hA! z|3QaiYz!w+xQ%9&Zg${JgQ*Ip_bg2rmmG`JkX^}&5gbZF!Z(gDD1s5{QwarPK(li- zW9y-CiQ`5Ug1ceN1w7lCxl=2}7c*8_XH8W7y0AICn19qZ`w}z0iCJ$tJ}NjzQCH90 zc!UzpKvk%3;`XfFi2;F*q2eMQQ5fzO{!`KU1T^J?Z64|2Z}b1b6h80_H%~J)J)kbM0hsj+FV6%@_~$FjK9OG7lY}YA zRzyYxxy18z<+mCBiX?3Q{h{TrNRkHsyF|eGpLo0fKUQ|19Z0BamMNE9sW z?vq)r`Qge{9wN|ezzW=@ojpVQRwp##Q91F|B5c`a0A{HaIcW>AnqQ*0WT$wj^5sWOC1S;Xw7%)n(=%^in zw#N*+9bpt?0)PY$(vnU9SGSwRS&S!rpd`8xbF<1JmD&6fwyzyUqk){#Q9FxL*Z9%#rF$} zf8SsEkE+i91VY8d>Fap#FBacbS{#V&r0|8bQa;)D($^v2R1GdsQ8YUk(_L2;=DEyN%X*3 z;O@fS(pPLRGatI93mApLsX|H9$VL2)o(?EYqlgZMP{8oDYS8)3G#TWE<(LmZ6X{YA zRdvPLLBTatiUG$g@WK9cZzw%s6TT1Chmw#wQF&&opN6^(D`(5p0~ zNG~fjdyRsZv9Y?UCK(&#Q2XLH5G{{$9Y4vgMDutsefKVVPoS__MiT%qQ#_)3UUe=2fK)*36yXbQUp#E98ah(v`E$c3kAce_8a60#pa7rq6ZRtzSx6=I^-~A|D%>Riv{Y`F9n3CUPL>d`MZdRmBzCum2K%}z@Z(b7#K!-$Hb<+R@Rl9J6<~ z4Wo8!!y~j(!4nYsDtxPIaWKp+I*yY(ib`5Pg356Wa7cmM9sG6alwr7WB4IcAS~H3@ zWmYt|TByC?wY7yODHTyXvay9$7#S?gDlC?aS147Ed7zW!&#q$^E^_1sgB7GKfhhYu zOqe*Rojm~)8(;b!gsRgQZ$vl5mN>^LDgWicjGIcK9x4frI?ZR4Z%l1J=Q$0lSd5a9 z@(o?OxC72<>Gun*Y@Z8sq@od{7GGsf8lnBW^kl6sX|j~UA2$>@^~wtceTt^AtqMIx zO6!N}OC#Bh^qdQV+B=9hrwTj>7HvH1hfOQ{^#nf%e+l)*Kgv$|!kL5od^ka#S)BNT z{F(miX_6#U3+3k;KxPyYXE0*0CfL8;hDj!QHM@)sekF9uyBU$DRZkka4ie^-J2N8w z3PK+HEv7kMnJU1Y+>rheEpHdQ3_aTQkM3`0`tC->mpV=VtvU((Cq$^(S^p=+$P|@} zueLA}Us^NTI83TNI-15}vrC7j6s_S`f6T(BH{6Jj{Lt;`C+)d}vwPGx62x7WXOX19 z2mv1;f^p6cG|M`vfxMhHmZxkkmWHRNyu2PDTEpC(iJhH^af+tl7~h?Y(?qNDa`|Ogv{=+T@7?v344o zvge%8Jw?LRgWr7IFf%{-h>9}xlP}Y#GpP_3XM7FeGT?iN;BN-qzy=B# z=r$79U4rd6o4Zdt=$|I3nYy;WwCb^`%oikowOPGRUJ3IzChrX91DUDng5_KvhiEZwXl^y z+E!`Z6>}ijz5kq$nNM8JA|5gf_(J-);?SAn^N-(q2r6w31sQh6vLYp^ z<>+GyGLUe_6eTzX7soWpw{dDbP-*CsyKVw@I|u`kVX&6_h5m!A5&3#=UbYHYJ5GK& zLcq@0`%1;8KjwLiup&i&u&rmt*LqALkIqxh-)Exk&(V)gh9@Fn+WU=6-UG^X2~*Q-hnQ$;;+<&lRZ>g0I`~yuv!#84 zy>27(l&zrfDI!2PgzQyV*R(YFd`C`YwR_oNY+;|79t{NNMN1@fp?EaNjuM2DKuG%W z5749Br2aU6K|b=g4(IR39R8_!|B`uQ)bun^C9wR4!8isr$;w$VOtYk+1L9#CiJ#F) z)L}>^6>;X~0q&CO>>ZBo0}|Ex9$p*Hor@Ej9&75b&AGqzpGpM^dx}b~E^pPKau2i5 zr#tT^S+01mMm}z480>-WjU#q`6-gw4BJMWmW?+VXBZ#JPzPW5QQm@RM#+zbQMpr>M zX$huprL(A?yhv8Y81K}pTD|Gxs#z=K(Wfh+?#!I$js5u8+}vykZh~NcoLO?ofpg0! zlV4E9BAY_$pN~e-!VETD&@v%7J~_jdtS}<_U<4aRqEBa&LDpc?V;n72lTM?pIVG+> z*5cxz_iD@3vIL5f9HdHov{o()HQ@6<+c}hfC?LkpBEZ4xzMME^~AdB8?2F=#6ff!F740l&v7FN!n_ zoc1%OfX(q}cg4LDk-1%|iZ^=`x5Vs{oJYhXufP;BgVd*&@a04pSek6OS@*UH`*dAp z7wY#70IO^kSqLhoh9!qIj)8t4W6*`Kxy!j%Bi%(HKRtASZ2%vA0#2fZ=fHe0zDg8^ zucp;9(vmuO;Zq9tlNH)GIiPufZlt?}>i|y|haP!l#dn)rvm8raz5L?wKj9wTG znpl>V@};D!M{P!IE>evm)RAn|n=z-3M9m5J+-gkZHZ{L1Syyw|vHpP%hB!tMT+rv8 zIQ=keS*PTV%R7142=?#WHFnEJsTMGeG*h)nCH)GpaTT@|DGBJ6t>3A)XO)=jKPO<# zhkrgZtDV6oMy?rW$|*NdJYo#5?e|Nj>OAvCXHg~!MC4R;Q!W5xcMwX#+vXhI+{ywS zGP-+ZNr-yZmpm-A`e|Li#ehuWB{{ul8gB&6c98(k59I%mMN9MzK}i2s>Ejv_zVmcMsnobQLkp z)jmsJo2dwCR~lcUZs@-?3D6iNa z2k@iM#mvemMo^D1bu5HYpRfz(3k*pW)~jt8UrU&;(FDI5ZLE7&|ApGRFLZa{yynWx zEOzd$N20h|=+;~w$%yg>je{MZ!E4p4x05dc#<3^#{Fa5G4ZQDWh~%MPeu*hO-6}2*)t-`@rBMoz&gn0^@c)N>z|Ikj8|7Uvdf5@ng296rq2LiM#7KrWq{Jc7;oJ@djxbC1s6^OE>R6cuCItGJ? z6AA=5i=$b;RoVo7+GqbqKzFk>QKMOf?`_`!!S!6;PSCI~IkcQ?YGxRh_v86Q%go2) zG=snIC&_n9G^|`+KOc$@QwNE$b7wxBY*;g=K1oJnw8+ZR)ye`1Sn<@P&HZm0wDJV* z=rozX4l;bJROR*PEfHHSmFVY3M#_fw=4b_={0@MP<5k4RCa-ZShp|CIGvW^9$f|BM#Z`=3&=+=p zp%*DC-rEH3N;$A(Z>k_9rDGGj2&WPH|}=Pe3(g}v3=+`$+A=C5PLB3UEGUMk92-erU%0^)5FkU z^Yx#?Gjyt*$W>Os^Fjk-r-eu`{0ZJbhlsOsR;hD=`<~eP6ScQ)%8fEGvJ15u9+M0c|LM4@D(tTx!T(sRv zWg?;1n7&)-y0oXR+eBs9O;54ZKg=9eJ4gryudL84MAMsKwGo$85q6&cz+vi)9Y zvg#u>v&pQQ1NfOhD#L@}NNZe+l_~BQ+(xC1j-+({Cg3_jrZ(YpI{3=0F1GZsf+3&f z#+sRf=v7DVwTcYw;SiNxi5As}hE-Tpt)-2+lBmcAO)8cP55d0MXS*A3yI5A!Hq&IN zzb+)*y8d8WTE~Vm3(pgOzy%VI_e4lBx&hJEVBu!!P|g}j(^!S=rNaJ>H=Ef;;{iS$$0k-N(`n#J_K40VJP^8*3YR2S`* zED;iCzkrz@mP_(>i6ol5pMh!mnhrxM-NYm0gxPF<%(&Az*pqoRTpgaeC!~-qYKZHJ z2!g(qL_+hom-fp$7r=1#mU~Dz?(UFkV|g;&XovHh~^6 z1eq4BcKE%*aMm-a?zrj+p;2t>oJxxMgsmJ^Cm%SwDO?odL%v6fXU869KBEMoC0&x>qebmE%y+W z51;V2xca9B=wtmln74g7LcEgJe1z7o>kwc1W=K1X7WAcW%73eGwExo&{SSTnXR+pA zRL)j$LV7?Djn8{-8CVk94n|P>RAw}F9uvp$bpNz<>Yw3PgWVJo?zFYH9jzq zU|S+$C6I?B?Jm>V{P67c9aRvK283bnM(uikbL=``ew5E)AfV$SR4b8&4mPDkKT&M3 zok(sTB}>Gz%RzD{hz|7(AFjB$@#3&PZFF5_Ay&V3?c&mT8O;9(vSgWdwcy?@L-|`( z@@P4$nXBmVE&Xy(PFGHEl*K;31`*ilik77?w@N11G7IW!eL@1cz~XpM^02Z?CRv1R z5&x6kevgJ5Bh74Q8p(-u#_-3`246@>kY~V4!XlYgz|zMe18m7Vs`0+D!LQwTPzh?a zp?X169uBrRvG3p%4U@q_(*^M`uaNY!T6uoKk@>x(29EcJW_eY@I|Un z*d;^-XTsE{Vjde=Pp3`In(n!ohHxqB%V`0vSVMsYsbjN6}N6NC+Ea`Hhv~yo@ z|Ab%QndSEzidwOqoXCaF-%oZ?SFWn`*`1pjc1OIk2G8qSJ$QdrMzd~dev;uoh z>SneEICV>k}mz6&xMqp=Bs_0AW81D{_hqJXl6ZWPRNm@cC#+pF&w z{{TT0=$yGcqkPQL>NN%!#+tn}4H>ct#L#Jsg_I35#t}p)nNQh>j6(dfd6ng#+}x3^ zEH`G#vyM=;7q#SBQzTc%%Dz~faHJK+H;4xaAXn)7;)d(n*@Bv5cUDNTnM#byv)DTG zaD+~o&c-Z<$c;HIOc!sERIR>*&bsB8V_ldq?_>fT!y4X-UMddUmfumowO!^#*pW$- z_&)moxY0q!ypaJva)>Bc&tDs?D=Rta*Wc^n@uBO%dd+mnsCi0aBZ3W%?tz844FkZD zzhl+RuCVk=9Q#k;8EpXtSmR;sZUa5(o>dt+PBe96@6G}h`2)tAx(WKR4TqXy(YHIT z@feU+no42!!>y5*3Iv$!rn-B_%sKf6f4Y{2UpRgGg*dxU)B@IRQ`b{ncLrg9@Q)n$ zOZ7q3%zL99j1{56$!W(Wu{#m|@(6BBb-*zV23M!PmH7nzOD@~);0aK^iixd%>#BwR zyIlVF*t4-Ww*IPTGko3RuyJ*^bo-h}wJ{YkHa2y3mIK%U%>PFunkx0#EeIm{u93PX z4L24jUh+37=~WR47l=ug2cn_}7CLR(kWaIpH8ojFsD}GN3G}v6fI-IMK2sXnpgS5O zHt<|^d9q}_znrbP0~zxoJ-hh6o81y+N;i@6M8%S@#UT)#aKPYdm-xlbL@v*`|^%VS(M$ zMQqxcVVEKe5s~61T77N=9x7ndQ=dzWp^+#cX}v`1bbnH@&{k?%I%zUPTDB(DCWY6( zR`%eblFFkL&C{Q}T6PTF0@lW0JViFzz4s5Qt?P?wep8G8+z3QFAJ{Q8 z9J41|iAs{Um!2i{R7&sV=ESh*k(9`2MM2U#EXF4!WGl(6lI!mg_V%pRenG>dEhJug z^oLZ?bErlIPc@Jo&#@jy@~D<3Xo%x$)(5Si@~}ORyawQ{z^mzNSa$nwLYTh6E%!w_ zUe?c`JJ&RqFh1h18}LE47$L1AwR#xAny*v9NWjK$&6(=e0)H_v^+ZIJ{iVg^e_K-I z|L;t=x>(vU{1+G+P5=i7QzubN=dWIe(bqeBJ2fX85qrBYh5pj*f05=8WxcP7do(_h zkfEQ1Fhf^}%V~vr>ed9*Z2aL&OaYSRhJQFWHtirwJFFkfJdT$gZo;aq70{}E#rx((U`7NMIb~uf>{Y@Fy@-kmo{)ei*VjvpSH7AU zQG&3Eol$C{Upe`034cH43cD*~Fgt?^0R|)r(uoq3ZjaJqfj@tiI~`dQnxfcQIY8o| zx?Ye>NWZK8L1(kkb1S9^8Z8O_(anGZY+b+@QY;|DoLc>{O|aq(@x2=s^G<9MAhc~H z+C1ib(J*&#`+Lg;GpaQ^sWw~f&#%lNQ~GO}O<5{cJ@iXSW4#};tQz2#pIfu71!rQ( z4kCuX$!&s;)cMU9hv?R)rQE?_vV6Kg?&KyIEObikO?6Nay}u#c#`ywL(|Y-0_4B_| zZFZ?lHfgURDmYjMmoR8@i&Z@2Gxs;4uH)`pIv#lZ&^!198Fa^Jm;?}TWtz8sulPrL zKbu$b{{4m1$lv0`@ZWKA|0h5U!uIwqUkm{p7gFZ|dl@!5af*zlF% zpT-i|4JMt%M|0c1qZ$s8LIRgm6_V5}6l6_$cFS# z83cqh6K^W(X|r?V{bTQp14v|DQg;&;fZMu?5QbEN|DizzdZSB~$ZB%UAww;P??AT_-JFKAde%=4c z*WK^Iy5_Y`*IZ+cF`jvkCv~Urz3`nP{hF!UT7Z&e;MlB~LBDvL^hy{%; z7t5+&Ik;KwQ5H^i!;(ly8mfp@O>kH67-aW0cAAT~U)M1u`B>fG=Q2uC8k}6}DEV=% z<0n@WaN%dDBTe*&LIe^r-!r&t`a?#mEwYQuwZ69QU3&}7##(|SIP*4@y+}%v^Gb3# zrJ~68hi~77ya4=W-%{<(XErMm>&kvG`{7*$QxRf(jrz|KGXJN3Hs*8BfBx&9|5sZ1 zpFJ1(B%-bD42(%cOiT@2teyYoUBS`L%<(g;$b6nECbs|ADH5$LYxj?i3+2^#L@d{%E(US^chG<>aL7o>Fg~ zW@9wW@Mb&X;BoMz+kUPUcrDQOImm;-%|nxkXJ8xRz|MlPz5zcJHP<+yvqjB4hJAPE zRv>l{lLznW~SOGRU~u77UcOZyR#kuJrIH_){hzx!6NMX z>(OKAFh@s2V;jk|$k5-Q_ufVe;(KCrD}*^oBx{IZq^AB|7z*bH+g_-tkT~8S$bzdU zhbMY*g?Qb;-m|0`&Jm}A8SEI0twaTfXhIc=no}$>)n5^cc)v!C^YmpxLt=|kf%!%f zp5L$?mnzMt!o(fg7V`O^BLyjG=rNa}=$hiZzYo~0IVX$bp^H-hQn!;9JiFAF<3~nt zVhpABVoLWDQ}2vEEF3-?zzUA(yoYw&$YeHB#WGCXkK+YrG=+t0N~!OmTN;fK*k>^! zJW_v+4Q4n2GP7vgBmK;xHg^7zFqyTTfq|0+1^H2lXhn6PpG#TB*``?1STTC#wcaj3 zG~Q9!XHZ#1oPZo zB6h(BVIW5K+S@JG_HctDLHWb;wobZ0h(3xr6(uUspOSK0WoSHeF$ZLw@)cpoIP|kL zu`GnW>gD$rMt}J0qa9kJzn0s`@JNy1Crkb&;ve|()+_%!x%us>1_Xz|BS>9oQeD3O zy#CHX#(q^~`=@_p$XV6N&RG*~oEH$z96b8S16(6wqH)$vPs=ia!(xPVX5o&5OIYQ%E(-QAR1}CnLTIy zgu1MCqL{_wE)gkj0BAezF|AzPJs=8}H2bHAT-Q@Vuff?0GL=)t3hn{$Le?|+{-2N~`HWe24?!1a^UpC~3nK$(yZ_Gp(EzP~a{qe>xK@fN zEETlwEV_%9d1aWU0&?U>p3%4%>t5Pa@kMrL4&S@ zmSn!Dllj>DIO{6w+0^gt{RO_4fDC)f+Iq4?_cU@t8(B^je`$)eOOJh1Xs)5%u3hf; zjw$47aUJ9%1n1pGWTuBfjeBumDI)#nkldRmBPRW|;l|oDBL@cq1A~Zq`dXwO)hZkI zZ=P7a{Azp06yl(!tREU`!JsmXRps!?Z~zar>ix0-1C+}&t)%ist94(Ty$M}ZKn1sDaiZpcoW{q&ns8aWPf$bRkbMdSgG+=2BSRQ6GG_f%Lu#_F z&DxHu+nKZ!GuDhb>_o^vZn&^Sl8KWHRDV;z#6r*1Vp@QUndqwscd3kK;>7H!_nvYH zUl|agIWw_LPRj95F=+Ex$J05p??T9_#uqc|q>SXS&=+;eTYdcOOCJDhz7peuvzKoZhTAj&^RulU`#c?SktERgU|C$~O)>Q^$T8ippom{6Ze0_44rQB@UpR~wB? zPsL@8C)uCKxH7xrDor zeNvVfLLATsB!DD{STl{Fn3}6{tRWwG8*@a2OTysNQz2!b6Q2)r*|tZwIovIK9Ik#- z0k=RUmu97T$+6Lz%WQYdmL*MNII&MI^0WWWGKTTi&~H&*Ay7&^6Bpm!0yoVNlSvkB z;!l3U21sJyqc`dt)82)oXA5p>P_irU*EyG72iH%fEpUkm1K$?1^#-^$$Sb=c8_? zOWxxguW7$&-qzSI=Z{}sRGAqzy3J-%QYz2Cffj6SOU|{CshhHx z6?5L$V_QIUbI)HZ9pwP9S15 zXc%$`dxETq+S3_jrfmi$k=)YO5iUeuQ&uX}rCFvz&ubO?u)tv|^-G_`h$pb+8vn@f z7@eQe#Kx|8^37a4d0GulYIUAW|@I5|NIh%=OqHU{(>(UhKvJ}i_X*>!Geb+Rs0MWf66Lf z-cQ(4QOENSbTX$6w_9w4{5eR?14#?)Jqf2UCk5US4bnz8!e>vFduH6(cZZ=5*_!M# zUTZ_b<4v@}dSQOcH@wt-s;3JhkVDct$6k9!ETdi-tplkaxl^qF=p}Q8KMVm+ zeIa2q?RYr}nM0d_W2YWv%JKyCrGSePj8GrRN)<$Nsq8l$X=>`W;?>0eME3|8t&d$~ zH`XG45lBh>-te_f0Mh0??)=Ee0~zESx=sZPv<#!sAVv$0qTn@CmCUNJU<#=`GC)&P z9zuV~9*3_n2*ZQBUh)2xIi;0yo)9XXJxM-VB*6xpyz{Rx2ZCvFnF$2aPcYFG( zyXkO(B30?mt;5GW&{m^w3?!P`#_o;Y%P2z^A`|4%Bt2@3G?C2dcSPNy1#HMXZ>{+L z3BE#xvqR@Ub}uKfzGC=RO|W%dJpUK#m8p&Dk|6Ub8S+dN3qxf9dJ_|WFdM9CSNQv~ zjaFxIX`xx-($#Fq+EI76uB@kK=B4FS0k=9(c8UQnr(nLQxa2qWbuJyD7%`zuqH|eF zNrpM@SIBy@lKb%*$uLeRJQ->ko3yaG~8&}9|f z*KE`oMHQ(HdHlb&)jIzj5~&z8r}w?IM1KSdR=|GFYzDwbn8-uUfu+^h?80e*-9h%Nr;@)Q-TI#dN1V zQPT2;!Wk)DP`kiY<{o7*{on%It(j0&qSv=fNfg3qeNjT@CW{WT<)1Eig!g9lAGx6& zk9_Zrp2I+w_f!LRFsgxKA}gO=xSPSY``kn=c~orU4+0|^K762LWuk_~oK{!-4N8p8 zUDVu0ZhvoD0fN8!3RD~9Bz5GNEn%0~#+E-Js}NTBX;JXE@29MdGln$Aoa3Nzd@%Z= z^zuGY4xk?r(ax7i4RfxA?IPe27s87(e-2Z_KJ(~YI!7bhMQvfN4QX{!68nj@lz^-& z1Zwf=V5ir;j*30AT$nKSfB;K9(inDFwbI^%ohwEDOglz}2l}0!#LsdS3IW43= zBR#E@135bu#VExrtj?)RH^PM(K4B`d=Z6^kix`8$C1&q)w1<&?bAS?70}9fZwZU7R z5RYFo?2Q>e3RW2dl&3E^!&twE<~Lk+apY?#4PM5GWJb2xuWyZs6aAH-9gqg${<1?M zoK&n+$ZyGIi=hakHqRu{^8T4h@$xl?9OM46t;~1_mPs9}jV58E-sp!_CPH4<^A|Q5 zedUHmiyxTc2zgdxU?4PyQ{ON@r+Ucn1kjWSOsh6WzLV~Bv&vWLaj#Xz4VSDs*F#@M>#e^ixNCQ-J|iC=LcB*M4WUb>?v6C z14^8h9Ktd1>XhO$kb-rRL}SFTH)kSu+Dwds$oed7qL)Jbd zhQys4$Uw~yj03)6Kq+K-BsEDftLgjDZk@qLjAyrb5UMeuO^>D43g%0GoKJ~TO0o!D z9E$WfxEDFTT?~sT?|!7aYY*mpt`}i;WTgY|Cb4{Cscrmzb(?UE+nz1wC3#QSjbg>N zleu?7MGaQ&FtejK#?07Uq$vIZX5FqR*a=(zUm`Fq$VUl){GQ{2MA)_j4H$U8FZ`=A z&GU_an)?g%ULunbBq4EUT7uT=vI6~uapKC|H6uz1#Rqt$G(!hE7|c8_#JH%wp9+F? zX`ZigNe9GzC(|Nr8GlmwPre3*Nfu+ zF=SHtv_g@vvoVpev$Jxs|F7CH`X5#HAI=ke(>G6DQQ=h^U8>*J=t5Z3Fi>eH9}1|6 znwv3k>D=kufcp= zAyK#v05qERJxS_ts79QVns}M?sIf(hCO0Q9hKe49a@PzvqzZXTAde6a)iZLw|8V-) ziK`-s)d(oQSejO?eJki$UtP0ped)5T1b)uVFQJq*`7w8liL4TX*#K`hdS!pY9aLD+ zLt=c$c_wt^$Wp~N^!_nT(HiDVibxyq2oM^dw-jC~+3m-#=n!`h^8JYkDTP2fqcVC& zA`VWy*eJC$Eo7qIe@KK;HyTYo0c{Po-_yp=>J(1h#)aH5nV8WGT(oSP)LPgusH%N$?o%U%2I@Ftso10xd z)Tx(jT_vrmTQJDx0QI%9BRI1i!wMNy(LzFXM_wucgJGRBUefc413a9+)}~*UzvNI{KL# z_t4U&srNV|0+ZqwL(<}<%8QtjUD8kSB&p$v^y}vuEC2wyW{aXp2{LTi$EBEHjVnS# z+4=G$GUllsjw&hTbh6z%D2j=cG>gkNVlh|24QUfD*-x9OMzTO93n*pE(U7Vz7BaL% z@(c!GbEjK~fH}sqbB1JNI!~b+AYb5le<-qxDA9&r2o)|epl9@5Ya7}yVkcM)yW6KY7QOX_0-N=)+M!A$NpG? z6BvZ8Tb}Pw(i9f7S00=KbWmNvJGL(-MsAz3@aR~PM$Z>t)%AiCZu?A|?P*~UdhhFT`;Nb)MxIg*0QlkYVX+46( zSd%WoWR@kYToK7)(J=#qUD-ss;4M&27w#03y6$gk6X<-VL8AJM@NFTx#Z!n)F5T357%njjKyjro(yW8ceP{!%;*Y>DN`&_18p(z2Hg$%K zohbgJcp%+ux%q6F?(sc_mYJ<$;DxgkTEi?yjT6Du@+n(KsKtFHcO%7O z=AsfLSTdE2>7a@0^`;)?Fg|s2XOPV&fo<%Q)Izaw4s&RvrX0^+aPNq|yE?oSa7 zsnNs!+vGcTM4yM|$9so*2Nv;ngDD}b0MjH6i4e|l^O`lzCRj)-qa6f%|afJpmf(S1J2k7Nt^!;Q}0 z4ejPF?^M~Sv+@LYn&IFUk2;1h?kb8lfrT`oMm=JBm{fo5N|HY~yQQ`T*e2?!tF%*t zf+ncx15$NdF82GXrpP5rJ7!PVE3>u`ME$9Hw5RlP zUh+s#pg{9kEOsAhvu2pry#@dvbB3Lti+9VkLxPZSl;fNr9}wv1cTahUw_Py7%Xp;C zaz__|kz*ydKiYbsqK{?cXhqR(!1KMoV-+!mz>3S8S`Va4kD#(aKyqecGXB^nF*>mS z1gG>fKZc?R~Tye>%x+43D8=e zf0eKr-)>VEu7^I{%T}BT-WaGXO3+x<2w2jwnXePdc2#BdofU6wbE)ZWHsyj=_NT3o z)kySji#CTEnx8*-n=88Ld+TuNy;x$+vDpZ)=XwCr_Gx-+N=;=LCE7CqKX9 zQ-0{jIr zktqqWCgBa3PYK*qQqd=BO70DfM#|JvuW*0%zmTE{mBI$55J=Y2b2UoZ)Yk z3M%rrX7!nwk#@CXTr5=J__(3cI-8~*MC+>R);Z)0Zkj2kpsifdJeH)2uhA|9^B;S$ z4lT3;_fF@g%#qFotZ#|r-IB*zSo;fokxbsmMrfNfJEU&&TF%|!+YuN=#8jFS4^f*m zazCA-2krJ-;Tkufh!-urx#z*imYo|n6+NDGT#*EH355(vRfrGnr*x z5PWMD7>3IwEh=lO^V>O>iLP~S!GjrvI5lx<7oOg(d;6uEFqo5>IwptBQz;`>zx`n$ zjZQ#Hb)qJdQy#ML&qcfmb$KT+f_1#uYNo7HHDY}7xAw8qbl;9LWO-cndfI=5$%jBw zb}K3U%88Fg^|&0Vc~99bKl|$3JzdawRZ|`7%1S<8B7>9*rWAT0U<@mHDfnL1`~1U| zDw7m@<@}C|zqeHM(OK@di6~sKHiJvk^I0^S<LBe^_xZsUOzVkYSE)Bxn*NekQYbyTn5SRt!n{EseOo-$u)vjM(PV%6cIG3Kv$>dd}HUyXi;_Lv>}OyUj38dPe8+1Pr?{LXnIBCoTnocD60@vhsz+GG5lJB9ncgP8T6@LwuzZ)J zKETBS~AvzGE!{u^+Rd-|Gn!rc@UUnioP0{@_j_>tg8YI#?y zL-H$=&xXkCJ2Qe7&exbI!z`OyPxBp|4_ zZrrc;OAb%T4Ze%7E}FBB`8t$QN0sA3vpwU>?7QAmE%-ethXdCtby$Qm3v$lNxB2a7 ze6F5eEWV`={#W(G)Va}7?$D65WF|f0nmfZT;?=LE6Yz{{W3CV2h^Ma+LXdZ(HMVKZ z!YXJ*34lo!FA>)jSo@*!Hs_)IwmTo6pBr3c^j2u_amZ~g;&Z2jZIw!}v@w8DtZz7|A%rFksD4^HYB!xFAqX;u0HxPeG!3Z(z z4}+^N5-nckKf2YSR5R_}PD+2?Wq#BOiON74#{`u=4f59WKdy_77EYq~_|X6cNtno{ zZ?WLwbV57Z6uI|uY_;vzv~~`eiiOl($Au7C*X<&MY5v0b`KEu-GW}{2UNfmmrP!^Y zAOczy!}TIJsom=}kxH)9W`&Rp&rR6T7y&~5nXbut;wcs@M?aa^9j{ZDtx=1?P8TV{ zee2kKf%CE$mogyKKT=xQQ#)OCl9bjc)}{p2X$}aG`^B0w0yi-rI!d4e-u9uR$kJK3 zhqBG9Wx<-3DFw5olJ6neF@hB;8o(r(GB_;p1i>}cjN`JNEZg-dlxtLL=8~gfLrBy_ z1~bGh{I>_xqh(}?%bCf1U6~K@+N*i}bTi+pUAW)oM0`D*PeJq=S(-|Plxe9OqxBRg zM((r)xkSH@j!8@+=cA4US0fDL&O?W~x=Mlu>7zvHO2sy7D5_7ulP+YMecP~}F0b*K z3oO2j{o&WHd<&UWcyA(&6hvBJv}qUZ!@R<(mwKB^;y3zeE1>LzbDWSkRD1|5MZPx( zxd=&MsQi1eE@@6W+4N`cF?yh!3R5JlAV--&RONWQ#?SbrQ95<@ag>C{jQmGXpQX{) z1dbFg1_`qLxuDZnX#PKfCW*Jl3F&^7@gO&{>Nb8um$VBcF1!AL=N6`A%BFj=`QaPI z+m^`n+{o)KLif;Gt|7aQ(XXRP@x)jJt}s{&S`I3}jPTY>$@W0BD3Oif^ehs~!H7T1FUSWxLS&W;0q6+azjbWn?3!q$ z9qbmdr4H4Y)p^NOACJ^L>u}NS8T0_5hW)G z%Hv}dAqM}d@t;|hf8>+NHHPi*xePsRlqr46njzhiXXZti7i5+GTKcrlxA->OJ9*Pna`02EIA5~(SMV`T@H6F2VtwwP1$tYujbC1^VE$Yd&I`WSwB^1( zT7NP3|85z#R%&wktjwY_i*n_$RRZPM^ota{LPV%*>=>sAv%fn*cnkCIX{^SJRmwZv z!?f@T&D%Lz@*!mNYTGp{J|7)~PR*ib`;l^E)rQw@)Qn0ECnB8W1S_SbLZWdqcmo?V zX5g0_3qhn4TrN27^x#Qdq*4*G1L|)I^b8GuP_8O{p|M`uvZO6McXa>OSQRW|kQTNPZ#Zyj~SZ<`6B)Y+}jxpn+YT>MhZ!Rxyd@rU>N zP>MkDBLX|<)SJaO?Ge=!D>i+Wq&PgneO?ZXUq4IQuTq z+V{ZGkuw77o~o$!b>4ov`6CKJ)$cf=S6%1ZQyYU!kz_qiuNxY2*Bh;K9J6o_YV6xQ znW|>x+#Mymu&wF9P|3wP*(ZjwE+ou|{eFqMv}d_iEyH zQ?NSf3VX+EpbrIKmp|oD-t_rh(D#e)fp)dYbG{=yPj-3-#l+iu7r+~#w|(#wv@G0` z38`Yhf5CznhyDEhD;jzaz7fc8L?(n-m zR#|5hqq#yRoeTm+h^9J42mnB>BY>HSu&&O-Hxo6j!dqck)dGS&odS@Hsk2-*Z~x z0!%{@gT645S5DeF@JZeE$DFl*nJB8Z|JKvs%7d`KjbJ*AsA_=fEZ&V9=*+K{(TF^( ztjjYr(7@fV^tDs9c*#=8)ZRKO17A5Z`8v*)U+?hS>3sEfgh3`#vFO^7n}&&adV?}n zdy&BY1h|I@eBm=l*kqiJn>vNkOH4l$Op5Hw3K_w8lF!6T@-H)S2W|Km#6!-X#NqLJ zsiVDrc%*@I3^Gen$)6O0C_qw;8{aucF;}U^1%YE`?AYTtb`Z$B$vfhcHQF`VCB(Pf z_G#fV*Colv-k!O+=^nDNe(03?m+RTu&28d%>JrrwFNb{ND&?Ad(=DP@voz$usk1|w z&#gTB7F)#*LtY6@pIb(g72*LcnXRlTPQAD?)ZFnB*EsZqxM&Uk_KGXnR{4}K`I6i- zU9}R>tiO0De1Hx=kAy>7O+nKO@kGQEYOai&S9&WTY+flvR?uhI695W-xZnq4aRMh8 zwfp)+KYWVB#r=5AwwlSdM4@x7-R_{2;1iqz2lXL$7iu1>5W*+I)jlkMs>60=LN)Y= zbPw;;%U+%p_&{2Obemh$BLmbpDd31YxJ8#TpH3~3B8QLUMvx1X5Vl48hWSNN*UTlO zQgQyZbmyjGC-s$3tnB z0mfKUu2+_c`ZVvDVwUy#j3W*l^BSXXQ%=r6Z}C73jx8DAk!t7k{dK^udpHIcUejp# zyx}og$Hr+f>9kaZvno*Om`d|VTUce9tHM=R8thoG!a=NT$s;g@n_rAN%cp7nnLuav z6}j56TSSfPL$p#y#!5TVyqa3zTzi7@#IoeR=E6CdS`JrR+@i2DwZ?T*bh+(k5!a)0 zgRdF93z8XJ|5?>hDN!YAW5cK=+BwDLNT_+otd zqC@*{S0hCKZ+TnN*2&qx+WP;ZjHA`yytPcwKl~)uy)sQ}Q*0-&3X|YFYAjmolaciq zxS$r5^fxICetD*Dw78M9leVvhAOZ$=;SP7L!Vs?+0f1h*YCuTXIt03iAf)0=0KEvZ zB69o-zg`0C#hQ>`4`}1g=a~EID(j9HbjJG^tV-zumR-+fahTPveA{%0u2uQwMZ%}5 zwY!|}i0oTd&>^QSRhIKU+cMC#|C3f>|647?v1B(wH)EWb{vuJEJh~!#|J7%=h!x3| zCH6m}wg;>Q&?@5Ct1%n`lj%*>9a52d@wmvE`=aQjtz$sWj3V;fDns5<7d2*``)u1( zh!Ub>!#N0m=Vz1n1=El zwb2IVRw$6NIFRpGyUoM0iqc$IPehcmm7<0s7F*Yv+zq?_%pf*SS~~}s0M`m(rMbx% zi?|Wjr6fJN`_J8&B2$4+V+iO~m>s~Zr2T3Y3HGREFQ%%pEoU0N));AeSVM#gYQ>l} z0`RhgS`R^pJH31YQ~eTeJiI}g$&^|nv{!h?8mJK{{XDt+sG8D`7)$jvM#hjPI(5sS zfFW4s7wao%Lo| z#pJRC?iZOai;57ANs|vm6%}rPlGo}}Aso1t#xJn}%VW@~1WSjh(@JTgM$0x6ZQ)gB zdiox3f>kqGZY}+R<;wlNoWJ8#X-v)1;wRD*ec*wnvsN06Q@cZuD`deT-Bu&G;2fBC z0FE1%pG@{Yo2O87&dE;w???%`9s1gs=3GpM8xx_}=AB$K9y=cD);^iE*p4;T1RU%B zBPr)yqOBX<2}xt%g9qr>;z&|?4vhhw7@$a}Uy2b%_^VdB^VfzrebKUPnq;hliCNU% zVt3R5EHkhN^Pv`REF+npA@#HdCQN9IbQbqSDs^+zt(A6;rLwN+@Em}WrV5vPEo!w^ zSCd3RZ8{7a@d9@|IF&&G%irS7FHle?@49LctrtTt=rP$W)se*#RkFmyf)D1^U6EYI zfh+N?uH?-))O$9zM19VsuGn8?o~5`scXU?!P@_cWP&1U4PQqGus=sQzrX+YvKG%XBL3nt6!&M<#}wqA;Mo(}qrq<1lNkpQD-T#-y>grt|E+JNU) z2j+g+QPcA9VEFc0k;H(hSNOpp$I+!$ z&d&W6kBM9+c{X%vr_X0}tdB5dvEDyk5H2*T(QW8Yz-#tjvF?up=^Kfym``^!&O-X! z@HdfpHn;}_)y$Xjb-5cR$Q#-XdhKpmJG5pl>h*Q2(u*gt_4(>6?kG)%T3*&TT0qI( zL!aR~4HiJiaHlgdNcOQP6xx1f3AWx&8}(NEps|G!cO>J^rE2@&-t#_Jb7GYgnLnML~1ze1D$?~BwbgA^=pr55tC|d7w42vN11_8bS75u z_MRKqE7Xik8fk>6(VE5{qT}6rSzd|o}Zb>*aI*Bwg%ccE$_ytH;g2H z^i3qY!+aE*&s^BMH9TI6GLm&9c`D6)3{-+?2Pon+040Yuv$2(LqV*krKhTg5CHOj* zquacxc1&~=S(O@gR8aI#?R%)meONmw1rub9E2QzeM$pBBm2wbPNR3tab{op53<oFwaUbARdD5jSA_6zmKX7!VicEP1m)rYnk{P- zruRj;4c8S29Rd#Baf|fq_pA^r3K#qRHS;($XNoLI*`puZjM?bA0tH>FDiVc9qR*|3 zGn#nhqxkvqFwRfCB~2yA0pxWapfjCdAem$utuon-`*6}mUP?l%$CE(FjAwL%Oe7GQbu7*+&q>*(cAofJr^gg>xw>hx-SO7Lx2)I} zJ)tV1XKbkE4sS&La#-smSq>S9gBzGLH%v?KVezdGv%Xs}kDJZJi{lDl(FpLZupBta z3iDlkd6LlkRro}+El?GIObw06D%NTXpL{W}Ve*%u#{wTC=+VHS%o`sAez&cYz|Tn` zcK_~pvN%cd^8FlFypCjTjw9@ulLoJ^!QAK*++^wC2~}CFeoY;q6y~r&f^+0>LR6)n z$hSev@GzzGgDc>)#u5_;{T9^5y5I?m=z7=J!eVId8p6R5>NV8)h|bA}#3KUufq4CPGiWYvGj%0=H@Q66);F)#cDMND4 zX|?rg>Bb28q*a!_sgVF(A=OeC&je$C4>$0%yy;Fla-hl(|9Ww4!@Q#E2hpJMMxpQ2L+R;+ZMpS+|j*F`Fh}p)`a_*<`AaeFzNEq^- zlF$7BFKD%p@K+3$Vx%N{QOayKKWU#JOAwXiLO62cA6=|DiDG_Z=ef;f&gQ5-?+Pb+ z)4NsyEZXCdjq5tgDN39V9!6#w25+R1;PD7ss;hFvQn}Hnl3^3h<`ylzJdVEL>|Jj0 zg>=Pscwx&;pWEzMn`ld**$1F-nhqlMuX;G{lWrT<<4$7MZ^*4a2hAMf)3eYiT$lRz&9({j<=%DWIRpgu zoOns@gF}AQ_6Y5RhySg7yMtJcYQap6^hgy{`zX1Zv26q4<)g@t%aIi|-lmcySuRN8*5f*$aEFi8o#kMKRCMnrAY~l`= zez#50^@Qo+6r508>iKfAbbc3JwCnjnmw;~=mlMG`(H8EJz7W6mh@mdinO&)#zHX=| z&|fo@s`;njVkkCMczSnp+TnW8YPU4w2&QmzEh1}orF~KlT=V+`!!rH|PtULCcL!P*m0EaN0Ad2qBw%Gs40jfu=%`N*k@z2-p?&B?Yum-p+h?7(!D^ z&f2Bn_#t!4HM2y^*1GN;U+_x8T$Z2>U9Yx;p_9Qf=ww z2hxO^*{%p9-CwMKz}C4mTi8xvqhivltE|}Kgq5MK@f6tBT&`@RYzsFFi>*eMZ0Z6Y zKBl`GOh!U%C+PXJ|7PF)V*~#8eS80D@v-NL2U&;i62W}k+vJAC+7xF`eq%c0b?{PVTcqiDr%6jLBdkVcTwLJSd313SP)1r=;2`cORbMzrhqZxMWcTWru5-l_H8;f|?{^M%%7>sU zGx2{fX*t;7SewS|NvPR-6F5p(ji7d}CK#%7y}jsPkgj%F5cUbQ?b7uWpYks^|DL*n zau%X$^(%wXMS3c;C4=p*#q>ahmLH5woLsn-YcZP~mH-rGnRyl#KU4MsLu+G3z90+q zM$HCWgZYR`8_I%8)SYuBltP$sN`-6hcjnzhDsVl+Y}yqMN*4MWsJX_6R>Cyw8cHGQ z1>r%vkDxxc#ACA4+-ZO|QBMUz`YHrS{l-*$> zi(n_;4{Gn+d2gn)TA<9) zibWdKJv#s_f5K}vM=d0NaYrd;5A+Fy^=+WgKC`@bS>!P5@K4fzE#VYfMcNdbbvLPY zeR~!f3xU>|pfq-LOsoF=t94x%K!8>#8tR4KQ2G3Yr?Cb98^KL*+G8``rHMpNUN}-T z5HGAkiLh{WR;N$Nk3X_2^3pW=vOFTOb(LS0Wu)0)I{8sZj>}5ZGtD=va-72l&5`L= zhyzBWie2UrC|?(sTcuk$OwvV4oVlxc3ncXPj|cD%%*6(hoKMd5wzPQs^6g)B0xK#d zemOodB7D(!@v!|eYqMfx@M#b+D)PwAuvimOW#13i-xAR5)Ai; zXNX(A@M*y&+TVZI zGHo$F*Ipg~Rnp`KlMNAl2o86}r%Yv9#!O-oo`pe`880;-Y28tR)b4H%nqXXHxN9m0 zI&#!(XhT=T3$WS$)K4#Y=ceN`MsP0v1X{nIoQ14S2^--MnUp21=V3&Uv8|y}^}7Vl zI5tRbOp#?@ay6uncZFE0hg}kt(k%piw^M8;0yynsK_!l~uP??IqzmKJMUqAW^GG{~ z7Fg)Q&zBlp z%Tj8jOUpuR>YHP6zYsX?)aJ`)_pRwu+Tn8I;brOW_`v$u$`$9T)cO*O$j=?mg>dW$ zw=&3=v||fqCr`-$okN*$S9(Nyrs}+Lu#IwDg2xSBz_VfU*?A&26vwv>&>*U_TT7-7 zS~X}fT%9+q(Xvc0qzOG^8gmMcZE9izi5feqvY(aY=%reP+wVZ&cRd`^y6}-gJ&_6n zR%Wdl3vQ4DOt!X9ry7j%=+7pLPdus*@7dZMBo0_WKZPD1(o{=;D> zyc9_WFI3{URv=d6EXcnOG0$(J(R#8Oz$kmuSFQ{-Y20}1027!FkodTU!fouSybwqn zRO-$2BH(w4)$wiPo<1w-4*p=Q0@YKRm^cgiA>~ho)U8^e>SBk*!@xvr0CdvnLHS#CACVuQfgzF>8qV znqf{oO1}RWhiZ3g!Tx9sk!JfLqcP`>Ksx#vZuLg-DC6h4mT!vlU zqw0`0CzZgY!EN0*{sQnDNFn;T<+e_x$zY|n;p0@d^hK*n!S!=#^;P{*D^6~h!T7r6 zoiMxtovMo-dj*{qZPy*c3gaMBEDQDkINU%d8HeBZVlRuzkCId9rx{?L= z-dLlk$w&JX5wn+8`mtqCpKnx+w+$@6DEUI}8P%xN$MEsw%S1-$9PM6r^jP-@?cS<# zhg$wl0X=s3{8EZ2U9(};p{X_b1@jJuGgx`gDK{6MpF|XON_=Rv%-<Ee1cuuy?nl9xVDa~x=+8ppnOQ9 zN$53qi4QQ!co(;f!#YJ8(=Z>_9UF#(QOVjS7T!g2)*Oecrf-R^)tFugBkQsMVNua# zS;1V^#fJS{h+!O+FgS%0=Pd9;lMa0QHn?-n(<0b2$<|@r>fjiyw6u*UoGmU$ayJM@ zfp;c4@{$b*Z_v9?8ZEp{m6Q(mDHW<``n?jg-ZN)Hhvxn*l=O1f*K%{5s77WCt!ugS?*2oG5-Q)JEJd0+W5=doeD$Wh?U$ZRg)K$v8cmQ{hba9jw_mF&X zi-dV?WITgIz!!0uB~jE?(t`&qo{WGyUspX| zc6+F2K4l5$LqxERF#`I&k^^opVIMZjGhsJ^vI0c%kV+|&_k>~}ueTtj;^Dfb@xHs` z)-39elzVA~D~n_aoyBQ1>Qd2!;E!G*pZM&RX`r*y)b`yxvP2;#vM*;CQGPg|gni)} z47`Log3PUyVfdmJ2zvHBhg7T#D-H=myzkeUa$@);WC(yB4k^*$wda3=S-UH5Q1Hx6 zPcGxMP&kXBa+4$s#Sw3-V?mlHj^8&bLpIN~GkYj;!;M!$ZxvtQY4j&Ngz_mxuQRqx zYTbN6epx@-!0jRV5yiSIJ<^mCZ<|;&x2~a)t+(eAVB!1XpCZok*Z2C5P7&>z-Oy?t zf@F(_FLsSrfCus61+Vt~svP%(u<4pzT5{w*0XqfPV%~|=%aq^$=*U+_trGQaoUxbt zBV#Yqx+ULku8yPJs4gGcC?+3iRt_6)Oi0DNLxdb(!n!cup_XUZ3eDe(!DChZ!IG&L?_;T-1GB!R;;Sk;l3Y*JQ!I|l20_f}ZyC;4D7R@6F z>%z~wV;Bj1b(*kp26Ed!Y-OKxNbt3%t))xxOrazWsmwvW;uaSaJ0ou+{01vXvU>_V z6Ha@+;giVaiyg`J8ENQf)Pq>!Nf22>XFHnXTNk84&jp-^YwmlUqnOll8)5mzlO$o! z#fSMwH8Pn+Fy7O5M5#ZGr$cKfaGf8g;XN)<*TrQjMk<}_oRf&b6qZoR38Q{Zxo{V; zby+J_hCZT1>`4~jnQxo|ji%BQ0=BLzC6c!1=B(jS5+fcp%q)JI)=c3{D|=k5;0&c2 zrbRE|qxkNqah2nvextOvjYA{T43n1c6eO7B9DH)tLqB46E7;0xKM=%#wx-*-+*OY{ zQ#7gMStz%I&2&rbo>#T20OD_#g`WYbt9+!MC08%zSMhqMoRk)7VOk%~`sD%(U6zzO zdmSC9@x0GCv2_)umYc5@#%efP0_cu+=f^}k$H9$N_>piA_(5UM_o{++8+Yf8SJ)?C zDd3l=GGm3EEy;&Z6N=+XP@IM0L=uW^ooyYQYyx1vwFR?@U~BAtAqTu%Mi2 zTCQh$K=UZA{P`Cw0I$xAh_f?fq-Goe`7I38{3L8?K3`lRhSAyB)tHT@4c!Y;bJAAS z3u>Q7qx>9SJs4$EB=hxh)u`W5jp?>^g1s_MV7<1zN zXt{FSt?Mt&8aCy67<)b@eg@h0iCW@%+pF-V>p${fyEk6_Gvp|ms{Whi-9eNId?xzZ zm|MI>F;JSuaUnQp#|}k3o&ddCZEeTI608txuU4~7K(wg9 zg%+}(7h2@(%>LI1F*puF(h$ZD`Q+ar!VoVajPY0-XS$>6F_F?sc6Mr7>SL-&{pC;2 zKx@2{@ULz7RCpaKg$iu2rcY+y*~qaPo0}^7T1K$_(NPS<1;V zTj8-xC%WvgDI_YYEG{bySvyO3M>XKY)oXgGG*eB{yDgNQ3s3)A~@n>!O#lNh0! z(-dqW#_z&mMfq#2+u61N`L^({4UoU8wE5`4c}{SGFzKb(BK8hM%cf_zj_HmC48)M& z398ICVJTGzBaz7K{L+Ew=;z^0xA``wbtPs`r+Wrb^_vzzhukq{;A`t&-ktzb zbqy`Z0#D6fdVAiodjF3J+qI*vu#=OCjiL4bIIXEf4?zmN7(H|+<+WfR7@7jrMx7FY z5*0X1enhay-q^M?j}3Pd^|U9(C3#CQU3=hlc~@y9@NQD{UZNfC^5?Cuuuu{ebn_<7 zEzudv*b@QP%)N^5jP;86nQGb<*SOytCM5wmf-=rH#K{Wd$2(X#S$jF}XIxZC1)zir zU2Wq>hIB44nCTqx2x<{_wiVzLSJR}L%P!Y|lFHtA_=bDj=OqvmmSZ}ffuqPge#V-f zZDk|XX0RK}=73LxL`H%OXxK*^I2!fp&kxatErK~&tM3@j1a(Yrq$z)R()i?}p|0^Y zhW&8!IpRA1jJ3e!p66ZY=eBmEA+$A`!%s+{Cz!s$IA`{_Dh0^jt!vn;+Nw}hx019Q z_Wg=#-G-~&@>l=&H~48$L8`LX)!Bcq%(DFa2Loc91u@WcwlHzJwo{cdur>bQ;{fr_ z`rC5QRQ_)`8EadJzz-{K&sUI~>NX>P|c4l)fKS0gkuGe_P ziaQy!%CK(CtAwj-J8&#kyU=G(k%3y`!gS9dU&1xIrGRL|!&aVMEaezUIpopoET~xE zp`%~`LZfn!Lu^+00?>v4UOfM!HeeQoLZP<#o`^9oi69|$0BM?n17R~tGpY)eJiv@$ zTV-~ZZ*}C1J{a}p`>l$Bx8qRBq91;dLdmp84auzmcd|XzJG%I|r z^E-8Tm~jRn_>as(R=@~z3I2E3<=#hXn>A=0`wfOGIxiP)N2%!cG?&^w=E#TR z`lSY@Mm36zu4p3}+S#67MpL$d{gf@dnP%*ZMW=gCXK-%0E(xAC!^+b7hCSMF$m;Rn zCTErbBK#;a)>kHX5}w6PRmnw(!Gy>m_g*2opfklHyx>eb1bu|_lwJdf!ogxhk}X^v zc+^L;F7ta!8+i%6?M}XvQn4b%aOSCpDW+4#JDDG(wvXC*9%9(XBhbv4LX3R5G&(+@ z)nbdivYRQ5pW;9~@YGf{h~Rm(@MfV8Tj&T@EejO6(C#(+z7FVNBR`@j!#wScHM5ki%j+^GykUJ2m zYgpwm;#Q)~LoozUSV($?r3vQ~#ZU_}ggl~J%z*1dYt_^4K6e7o&qs_ORz{km+D+^a zqDdUO)d}|)v9h(Zz3}#DLWyRVCY!=PMCO{=PA)Upb@)1j?c)||l{6&pI=;U#bS#Jk zOOiwVH3FM!SuJDIPnN$|ZKz5fQwHmzn8f^?B+T2ew%~PSE#X_jk`Wu;a{4}9%AHg7 zZm8^bAee$bdpwklIE`$fV15=pI+tgJpll4uQjIM;Q!gvISFc_{@=lUSc-lABE%U?+ zHW$;!NcH1&F;AS~7RH=n<=!NTKnm3t`B@YeL?8d2{WGrmSjG;yBbY*9$N&DT^e?l2 z|1A2482Or7n7KF_TpRn|nmqD}`-=?QJ0z5q$C9Td^sML&aN7OGi+W$uYjDXKJg+0W@S=FoQP2dBI=48|FH>p2mh zFrdu!AwoG$NkvnZp_KT8HEo=RNNJ4IxucGXLr2N*I5Ao>Efb+pNOm9Zw0_7_s|9ac zS6}W##>$W*cBmksip;43p#a4&iTpM)8(gRGekW+AKm5zb)xpUFT>~b+FOH`Zs!$RDgpSCE z>;CL8Uu|EWeR~TvgDX@K=mtReFed;FZ!M2SjzW35i;UqfyemM?rq5yZS#hK5Y~|wt z2#^`Q6$b~uGT_++C3+B~#(oFHdSL&hh`Z8{t5#=ZkoaWVJoLm)3vT_@5HOnZGa;s~ z;4=E`3Eo@=$BxFjS`Iu|8SALB`<#TPTeE%h(dol+#CzJ=Zb&EHpw*=0H*~8x6 z`G`b<@>L2(AS*J!NVp`DN{g!8R#h(~URslf zC8PwGM$5V}+$WcoT*C~*$WmCpS6Gis&sZo|9OfRiwjX$f*&25Gjv6$YPde1smwGw( zb@y=gbl1!8>hm-il3&~zFca0~aJN!?b97+$E>2$Gn$31OR&UnE=Tm= zH44$Dx2HNN1lrCGjfuwo@+(m2j85w-oxre9FopupEV+6HACFyTbt}s-`lCCJ8om5RIE~T#Yg_DWu1u zyAp%jp;3&%D4;CRaR6g=f*ZvPqw2BadP=*ZYy_~CV3@wFx5YA(E8)jfqx z8tjEkMf>msMqi)zaY2fWrMq`lZzZdiMcluc(@(yxK(4hPEFk0~HO3^CUZk3;?Tv3` ze-rjZ8@hBrVPzA$^4hW?<33{d2)h7Jw?$t%V6(C_m+bNhXl9vXCJcBWmMeQoLDm5b zt9|A5pDHY#Y@(rlEo_WzXila!uaZE*WVc`=IM)SSc`#liZ2Wt*~fHgm9uH^ISX2d@)XGZ)_$qnbx6?J<14_=SS(ITs#LPDk03a&%x;bAuGz=P ze^<4p@tD@J|M;88;~IsEOPpB+&3C4!3q;}Kk2tb*WuuE z2u(BE$1(2AwbbBrmU-YLI4>#K((6&QZ~m2Yp;I14x0N8hos}{uoQuMG)Wy?ogaNayqmc&`I=8y6&dPf{Fky#B7 z#F=Xy213s`NFxjKuMqH3+ibWsFRi=QtH*j$9^)Zy8F|^vSmgj~l5<04MiU;BNyAn) zlM+c20Y#%@>WgdY>5kx}H)7*!D~BZJdg8d5iHx|>(jj=!MEmr)-$kH8?A#;DyBone(uz;e^|=9nIwfuWY?yw; zC|H`;8#O$vTPm5AW1Gg-Up&#Ca$<@!JZkAUDbmd*?X}QSA5$(*c+FZ|l+}F%*L1OH z{ck}P=j@=7>6ga#cqzj|ODXHD>ckIBmOd9Fh=~>?C7$uII_3rEX%UKdywsInR~{t- zg|t`~l=L1P_QPkZN53Q>!^A*QDZ zK(f;%VVQo)n1bsy)LWL#?&|wN`hL~Rnxhd3d-bOvlRQAiybH&=i;SlnwP$3P-!%x3^o)t6aoT-zXU}ARq-l^bOW-zg$@b|19Aua zF+k$V!uO;fNwCUEi;6!|5?4_MKtTq}|C`2gXh8EhWP1bTgZ)DqHZ&-x|E2*6Ka!RZ zS5jsHN&IW7%g1yUln@bn$cO!hR2b+`P~1-3dFIx!6EltRa{a z6Z@Y$_ug)~d%u)K$+?LYfc<87}bupdiK(3|m%hiA$Pc>zKNP0hqBj{X*L0rm@j(0s(f>>t{1L0?w#rS+#E)IdBKcF5|Dq-S zZ*-X3x;NeSuOSxS<3Q%uy1zwQ+?Kj&)Ou~-|2+&J{Zi^T=lx9+&+B^K_lQ;hY2H6D zeZ9T!H&;?$+kt+MLCs%i{8QEVi8<(Pft!mFt`}r~k5Y%93jAjQ!fgoD?Zh|Vi~q5A z27G^+_!lc1Zfo3}625-J{(B@p`IW|R4(!c|yX*Pn?*SA0)3iUGUB11uH>ab1{F$$g z|7q4=O#$9cezU54J)`wKI1_%J{14{0Zj0P3wEcKU`%-=?@(1PW+Zs0qGuI`%??IID dD~*3C;60WFKt@K_BOwYX49GZ$DDV2e{|AYb(KrAA literal 0 HcmV?d00001 diff --git a/codehive-worker/gradle/wrapper/gradle-wrapper.properties b/codehive-worker/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..23449a2 --- /dev/null +++ b/codehive-worker/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/codehive-worker/gradlew b/codehive-worker/gradlew new file mode 100755 index 0000000..adff685 --- /dev/null +++ b/codehive-worker/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/codehive-worker/gradlew.bat b/codehive-worker/gradlew.bat new file mode 100644 index 0000000..c4bdd3a --- /dev/null +++ b/codehive-worker/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/codehive-worker/settings.gradle.kts b/codehive-worker/settings.gradle.kts new file mode 100644 index 0000000..85d6670 --- /dev/null +++ b/codehive-worker/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "worker" diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/WorkerApplication.java b/codehive-worker/src/main/java/com/github/codehive/worker/WorkerApplication.java new file mode 100644 index 0000000..ce6d329 --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/WorkerApplication.java @@ -0,0 +1,13 @@ +package com.github.codehive.worker; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class WorkerApplication { + + public static void main(String[] args) { + SpringApplication.run(WorkerApplication.class, args); + } + +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/config/DockerClientConfig.java b/codehive-worker/src/main/java/com/github/codehive/worker/config/DockerClientConfig.java new file mode 100644 index 0000000..da8ffbb --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/config/DockerClientConfig.java @@ -0,0 +1,5 @@ +package com.github.codehive.worker.config; + +public class DockerClientConfig { + +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/config/RabbitMQConfig.java b/codehive-worker/src/main/java/com/github/codehive/worker/config/RabbitMQConfig.java new file mode 100644 index 0000000..c3a0a18 --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/config/RabbitMQConfig.java @@ -0,0 +1,5 @@ +package com.github.codehive.worker.config; + +public class RabbitMQConfig { + +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/SubmissionListener.java b/codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/SubmissionListener.java new file mode 100644 index 0000000..8f1466b --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/SubmissionListener.java @@ -0,0 +1,5 @@ +package com.github.codehive.worker.messaging.listener; + +public class SubmissionListener { + +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/messaging/model/SubmissionMessage.java b/codehive-worker/src/main/java/com/github/codehive/worker/messaging/model/SubmissionMessage.java new file mode 100644 index 0000000..e69de29 diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/ExecutionResult.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/ExecutionResult.java new file mode 100644 index 0000000..c7075ff --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/ExecutionResult.java @@ -0,0 +1,5 @@ +package com.github.codehive.worker.sandbox; + +public class ExecutionResult { + +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/LanguageExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/LanguageExecutor.java new file mode 100644 index 0000000..b439d3c --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/LanguageExecutor.java @@ -0,0 +1,5 @@ +package com.github.codehive.worker.sandbox; + +public interface LanguageExecutor { + +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/SandboxExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/SandboxExecutor.java new file mode 100644 index 0000000..fc113e0 --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/SandboxExecutor.java @@ -0,0 +1,5 @@ +package com.github.codehive.worker.sandbox; + +public class SandboxExecutor { + +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/c/CExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/c/CExecutor.java new file mode 100644 index 0000000..f8533b2 --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/c/CExecutor.java @@ -0,0 +1,11 @@ +package com.github.codehive.worker.sandbox.c; + +import org.springframework.stereotype.Component; + +import com.github.codehive.worker.sandbox.LanguageExecutor; + + +@Component("c") +public class CExecutor implements LanguageExecutor { + +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/cpp/CPPExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/cpp/CPPExecutor.java new file mode 100644 index 0000000..b26fe45 --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/cpp/CPPExecutor.java @@ -0,0 +1,10 @@ +package com.github.codehive.worker.sandbox.cpp; + +import org.springframework.stereotype.Component; + +import com.github.codehive.worker.sandbox.LanguageExecutor; + +@Component("cpp") +public class CPPExecutor implements LanguageExecutor { + +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/factory/LanguageExecutorFactory.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/factory/LanguageExecutorFactory.java new file mode 100644 index 0000000..9d603a3 --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/factory/LanguageExecutorFactory.java @@ -0,0 +1,20 @@ +package com.github.codehive.worker.sandbox.factory; + +import java.util.Map; + +import org.springframework.stereotype.Component; + +import com.github.codehive.worker.sandbox.LanguageExecutor; + +@Component +public class LanguageExecutorFactory { + private final Map executors; + + public LanguageExecutorFactory(Map executors) { + this.executors = executors; + } + + public LanguageExecutor getExecutor(String language) { + return executors.get(language); + } +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java new file mode 100644 index 0000000..6d0c8ba --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java @@ -0,0 +1,10 @@ +package com.github.codehive.worker.sandbox.java; + +import org.springframework.stereotype.Component; + +import com.github.codehive.worker.sandbox.LanguageExecutor; + +@Component("java") +public class JavaExecutor implements LanguageExecutor { + +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/python/PythonExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/python/PythonExecutor.java new file mode 100644 index 0000000..6ed9682 --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/python/PythonExecutor.java @@ -0,0 +1,10 @@ +package com.github.codehive.worker.sandbox.python; + +import org.springframework.stereotype.Component; + +import com.github.codehive.worker.sandbox.LanguageExecutor; + +@Component("python") +public class PythonExecutor implements LanguageExecutor { + +} diff --git a/codehive-worker/src/main/resources/application.properties b/codehive-worker/src/main/resources/application.properties new file mode 100644 index 0000000..92fef53 --- /dev/null +++ b/codehive-worker/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=worker diff --git a/codehive-worker/src/test/java/com/github/codehive/worker/TestWorkerApplication.java b/codehive-worker/src/test/java/com/github/codehive/worker/TestWorkerApplication.java new file mode 100644 index 0000000..b6d8b84 --- /dev/null +++ b/codehive-worker/src/test/java/com/github/codehive/worker/TestWorkerApplication.java @@ -0,0 +1,11 @@ +package com.github.codehive.worker; + +import org.springframework.boot.SpringApplication; + +public class TestWorkerApplication { + + public static void main(String[] args) { + SpringApplication.from(WorkerApplication::main).with(TestcontainersConfiguration.class).run(args); + } + +} diff --git a/codehive-worker/src/test/java/com/github/codehive/worker/TestcontainersConfiguration.java b/codehive-worker/src/test/java/com/github/codehive/worker/TestcontainersConfiguration.java new file mode 100644 index 0000000..0383318 --- /dev/null +++ b/codehive-worker/src/test/java/com/github/codehive/worker/TestcontainersConfiguration.java @@ -0,0 +1,8 @@ +package com.github.codehive.worker; + +import org.springframework.boot.test.context.TestConfiguration; + +@TestConfiguration(proxyBeanMethods = false) +class TestcontainersConfiguration { + +} diff --git a/codehive-worker/src/test/java/com/github/codehive/worker/WorkerApplicationTests.java b/codehive-worker/src/test/java/com/github/codehive/worker/WorkerApplicationTests.java new file mode 100644 index 0000000..e5e5b7f --- /dev/null +++ b/codehive-worker/src/test/java/com/github/codehive/worker/WorkerApplicationTests.java @@ -0,0 +1,15 @@ +package com.github.codehive.worker; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; + +@Import(TestcontainersConfiguration.class) +@SpringBootTest +class WorkerApplicationTests { + + @Test + void contextLoads() { + } + +} From 0cb9a780e0103045997d71a8c53035f5b31b7760 Mon Sep 17 00:00:00 2001 From: IrminDev Date: Thu, 15 Jan 2026 23:36:39 -0600 Subject: [PATCH 02/32] Updated AGENTS.md and added missing files --- AGENTS.md | 210 +++++++++++++++++- .../worker/service/ExecutionService.java | 5 + .../codehive/worker/util/TimeoutUtils.java | 5 + 3 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/service/ExecutionService.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/util/TimeoutUtils.java diff --git a/AGENTS.md b/AGENTS.md index d5a14c3..4153414 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,6 +40,22 @@ CodeHive/ │ ├── codehive-worker/ # Code execution worker service │ ├── src/main/java/com/github/codehive/worker/ +│ │ ├── config/ # RabbitMQ and Docker client configuration +│ │ ├── messaging/ +│ │ │ ├── listener/ # RabbitMQ message listeners (SubmissionListener) +│ │ │ └── model/ # Message DTOs (SubmissionMessage) +│ │ ├── sandbox/ # Code execution in isolated Docker containers +│ │ │ ├── java/ # JavaExecutor for Java code +│ │ │ ├── python/ # PythonExecutor for Python code +│ │ │ ├── c/ # CExecutor for C code +│ │ │ ├── cpp/ # CPPExecutor for C++ code +│ │ │ ├── factory/ # LanguageExecutorFactory for strategy pattern +│ │ │ ├── ExecutionResult.java +│ │ │ ├── LanguageExecutor.java (interface) +│ │ │ └── SandboxExecutor.java +│ │ ├── service/ # Business logic services (empty scaffolding) +│ │ └── util/ # Utility classes (empty scaffolding) +│ ├── src/test/java/ # Tests with Testcontainers configuration │ ├── build.gradle.kts # Spring Boot 4.0.1, RabbitMQ, Docker Java client │ └── settings.gradle.kts │ @@ -118,6 +134,11 @@ docker-compose down ### Worker (codehive-worker) +**Prerequisites:** +- Java 21 +- Docker (for executing code in isolated containers) +- RabbitMQ running (via backend's docker-compose) + **Development:** ```bash cd codehive-worker @@ -125,13 +146,20 @@ cd codehive-worker # Build ./gradlew build -# Run +# Run (connects to RabbitMQ on localhost:5672) ./gradlew bootRun # Test ./gradlew test ``` +**Architecture:** +- Listens to RabbitMQ queues for code submission messages +- Executes code in isolated Docker containers per language +- Supports Java, Python, C, and C++ +- Uses Docker Java API for container management +- Enforces execution timeouts and resource limits + ### Frontend (codehive-frontend) **Prerequisites:** @@ -357,6 +385,102 @@ class AuthServiceTest { } ``` +### Worker Java Patterns + +#### Package Organization +- **config/**: Spring configuration beans (RabbitMQ, Docker client) +- **messaging/**: RabbitMQ message handling + - **listener/**: `@RabbitListener` components + - **model/**: Message DTOs +- **sandbox/**: Code execution in Docker containers + - **{language}/**: Language-specific executor implementations (java, python, c, cpp) + - **factory/**: `LanguageExecutorFactory` for strategy pattern + - Core interfaces and result models +- **service/**: Business logic (scaffolding for future implementation) +- **util/**: Utility classes (scaffolding for future implementation) + +#### Language Executor Pattern + +**Interface** (`sandbox/LanguageExecutor.java`): +```java +public interface LanguageExecutor { + // Execute code in sandboxed Docker container + // Return ExecutionResult with stdout, stderr, exit code, execution time +} +``` + +**Implementation** (e.g., `sandbox/java/JavaExecutor.java`): +```java +@Component("java") // Bean name matches language identifier +public class JavaExecutor implements LanguageExecutor { + // 1. Create Docker container with language-specific image + // 2. Write code to temporary file/volume + // 3. Compile (if needed) and execute with timeout + // 4. Capture output and cleanup container + // 5. Return ExecutionResult +} +``` + +**Supported Languages**: +- **Java**: `@Component("java")` - JavaExecutor +- **Python**: `@Component("python")` - PythonExecutor +- **C**: `@Component("c")` - CExecutor +- **C++**: `@Component("cpp")` - CPPExecutor + +**Factory Pattern** (`sandbox/factory/LanguageExecutorFactory.java`): +```java +@Component +public class LanguageExecutorFactory { + private final Map executors; + + // Spring injects all LanguageExecutor beans by bean name + public LanguageExecutorFactory(Map executors) { + this.executors = executors; + } + + public LanguageExecutor getExecutor(String language) { + return executors.get(language); // "java", "python", "c", "cpp" + } +} +``` + +#### Messaging Pattern + +**Message DTO** (`messaging/model/SubmissionMessage.java`): +```java +// Currently empty scaffolding - will contain: +// - submissionId +// - userId +// - language (java, python, c, cpp) +// - sourceCode +// - testCases (optional) +// - timeout +``` + +**Listener** (`messaging/listener/SubmissionListener.java`): +```java +// Currently empty scaffolding - will contain: +@Component +public class SubmissionListener { + // @RabbitListener(queues = "code.submission.queue") + // public void handleSubmission(SubmissionMessage message) { + // 1. Get executor from factory + // 2. Execute code in sandbox + // 3. Send result back to backend via RabbitMQ + // } +} +``` + +#### Sandbox Execution Flow + +1. **Message arrives** → `SubmissionListener` receives from RabbitMQ queue +2. **Factory dispatch** → `LanguageExecutorFactory.getExecutor(language)` +3. **Container creation** → Executor creates isolated Docker container +4. **Code execution** → Run code with timeout and resource limits +5. **Result collection** → Capture stdout/stderr, exit code, timing +6. **Cleanup** → Remove container, temporary files +7. **Result publishing** → Send `ExecutionResult` back to backend queue + ### Frontend TypeScript/React Patterns #### Routing @@ -678,6 +802,37 @@ void login_WithWrongPassword_ThrowsIncorrectCredentialsException() { ## Common Tasks +### Adding a New Language Executor to Worker + +1. **Create Executor Class** in `sandbox/{language}/{Language}Executor.java`: + ```java + @Component("languagename") // Bean name must match language identifier + public class LanguageExecutor implements LanguageExecutor { + // Implement execution logic + } + ``` + +2. **Implement Execution Logic**: + - Create Docker container with appropriate image (e.g., `openjdk:21`, `python:3.11`, `gcc:latest`) + - Mount code as volume or write to container filesystem + - Set resource limits (CPU, memory, network: none) + - Execute with timeout (use Resilience4j TimeLimiter) + - Capture stdout, stderr, exit code + - Clean up container (`docker rm -f`) + +3. **Add Dependencies** (if needed): + - Update `build.gradle.kts` with language-specific libraries + - Add Docker image pull logic in executor constructor + +4. **Test Execution**: + - Create test class in `src/test/java/` + - Use Testcontainers to verify Docker execution + - Test edge cases: infinite loops, memory leaks, compilation errors + +5. **Factory Auto-Discovery**: + - No changes needed - `LanguageExecutorFactory` auto-discovers by bean name + - Verify: `factory.getExecutor("languagename")` returns your executor + ### Adding a New API Endpoint 1. **Define Request DTO** in `model/request/{domain}/{Action}Request.java`: @@ -789,6 +944,28 @@ void login_WithWrongPassword_ThrowsIncorrectCredentialsException() { 10. **OpenAPI Documentation**: Always add `@Operation` and `@ApiResponses` to controller methods. Swagger UI auto-updates on application restart. +### Worker + +1. **Docker Daemon Required**: Worker service requires Docker daemon running on the host. Ensure Docker socket is accessible (`/var/run/docker.sock` on Unix). + +2. **Language Bean Names**: Executor bean names (`@Component("java")`) **must** match the language identifiers used in `SubmissionMessage`. Case-sensitive. + +3. **Container Cleanup**: Always clean up Docker containers after execution, even on errors. Use try-finally or `@PreDestroy` hooks. + +4. **Execution Timeouts**: Implement timeouts using Resilience4j `TimeLimiter` (already in dependencies). Default should be 10-30 seconds per execution. + +5. **Resource Limits**: Set Docker container resource limits: `--memory=512m`, `--cpus=1.0`, `--network=none` for security. + +6. **Image Availability**: Worker assumes Docker images are pre-pulled. Add image pull logic in executor constructors or startup to avoid delays. + +7. **RabbitMQ Connection**: Worker connects to RabbitMQ on localhost:5672 by default. Configure via `spring.rabbitmq.*` properties if different. + +8. **Message Acknowledgement**: Use manual acknowledgement for RabbitMQ messages. Only ack after successful execution and result publishing. + +9. **Testcontainers**: Worker tests use Testcontainers (see `TestcontainersConfiguration.java`). Requires Docker for running tests. + +10. **Security Isolation**: Never trust user code. Containers must run with `--network=none`, read-only filesystem where possible, and no privileged access. + ### Frontend 1. **API URL Configuration**: Set `VITE_API_URL` in `.env` for non-localhost backends. Defaults to `http://localhost:8080`. @@ -815,12 +992,14 @@ void login_WithWrongPassword_ThrowsIncorrectCredentialsException() { ### Infrastructure -1. **RabbitMQ Integration**: Currently in development on `feature/rabbitMQ` branch. Worker service will consume code execution jobs from RabbitMQ queue. +1. **RabbitMQ Integration**: Currently in development on `feature/rabbitMQ` branch. Worker service will consume code execution jobs from RabbitMQ queue (`code.submission.queue`). -2. **Docker Networking**: Backend, worker, PostgreSQL, and RabbitMQ communicate via Docker Compose network. Check `docker-compose.yaml` for service names. +2. **Docker Networking**: Backend, worker, PostgreSQL, and RabbitMQ communicate via Docker Compose network. Check `docker-compose.yaml` for service names. Worker accesses host Docker daemon via socket mount. 3. **Port Conflicts**: Ensure ports 5432 (PostgreSQL), 5672/15672 (RabbitMQ), 8080 (backend), 3000/5173 (frontend) are available. +4. **Worker Deployment**: In production, worker should run on separate machines with Docker installed. Use Docker-in-Docker or bind mount Docker socket with caution (security implications). + --- ## Troubleshooting @@ -836,6 +1015,20 @@ void login_WithWrongPassword_ThrowsIncorrectCredentialsException() { **Issue**: Tests fail with database errors **Solution**: Check `application-test.properties` is configured for H2, not PostgreSQL +### Worker Won't Start + +**Issue**: `Cannot connect to Docker daemon` +**Solution**: Ensure Docker daemon is running: `docker info` or `systemctl start docker` + +**Issue**: `Connection refused` to RabbitMQ +**Solution**: Start RabbitMQ via backend's `docker-compose up -d rabbitmq` + +**Issue**: Container creation fails +**Solution**: Pull required images manually: `docker pull openjdk:21`, `docker pull python:3.11`, etc. + +**Issue**: Tests fail with Testcontainers errors +**Solution**: Ensure Docker is accessible for tests. Check Docker socket permissions. + ### Frontend Can't Connect to Backend **Issue**: CORS errors in browser console @@ -862,15 +1055,19 @@ void login_WithWrongPassword_ThrowsIncorrectCredentialsException() { ## Additional Resources - **Spring Boot Docs**: https://spring.io/projects/spring-boot +- **Spring AMQP (RabbitMQ)**: https://spring.io/projects/spring-amqp +- **Docker Java Client**: https://github.com/docker-java/docker-java - **React Router v7 Docs**: https://reactrouter.com/ - **Bucket4j (Rate Limiting)**: https://bucket4j.com/ - **Jacoco (Coverage)**: https://www.jacoco.org/jacoco/ +- **Testcontainers**: https://testcontainers.com/ +- **RabbitMQ Docs**: https://www.rabbitmq.com/documentation.html --- ## Future Enhancements -1. **Code Execution**: Complete worker service integration with sandboxed Docker containers +1. **Code Execution**: Complete worker service integration with sandboxed Docker containers (IN PROGRESS) 2. **Real-time Collaboration**: WebSocket support for live code editing 3. **Group Management**: Teacher/student group creation and assignment submission 4. **File Upload**: Support for uploading code files and project archives @@ -880,9 +1077,14 @@ void login_WithWrongPassword_ThrowsIncorrectCredentialsException() { 8. **Admin Dashboard**: Frontend admin panel for user management 9. **API Versioning**: Add `/v1/` prefix to API routes for future compatibility 10. **Monitoring**: Add Actuator endpoints and Prometheus metrics +11. **Multi-file Support**: Worker support for projects with multiple source files +12. **Test Case Validation**: Automated test case execution and grading +13. **Language Extensions**: Add support for Go, Rust, JavaScript, TypeScript +14. **Execution History**: Store and display past code execution results --- **Last Updated**: 2025-01-15 **Current Branch**: `feature/rabbitMQ` **Project Status**: Active Development (Alpha) +**Worker Status**: Architecture defined, implementation scaffolded (config stubs, empty listeners, language executors with bean naming) diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/service/ExecutionService.java b/codehive-worker/src/main/java/com/github/codehive/worker/service/ExecutionService.java new file mode 100644 index 0000000..f8a1175 --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/service/ExecutionService.java @@ -0,0 +1,5 @@ +package com.github.codehive.worker.service; + +public class ExecutionService { + +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/util/TimeoutUtils.java b/codehive-worker/src/main/java/com/github/codehive/worker/util/TimeoutUtils.java new file mode 100644 index 0000000..1a00535 --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/util/TimeoutUtils.java @@ -0,0 +1,5 @@ +package com.github.codehive.worker.util; + +public class TimeoutUtils { + +} From e0bc3edee774ca313cd400d7003c6ef7b2b801dc Mon Sep 17 00:00:00 2001 From: IrminDev Date: Tue, 20 Jan 2026 23:24:20 -0600 Subject: [PATCH 03/32] Added the main model and folder structure proposal for the bucket --- codehive-backend/docker-compose.yaml | 14 ++++++ .../codehive/model/entity/Assignment.java | 25 +++++++++++ .../codehive/model/entity/Execution.java | 15 +++++++ .../codehive/model/entity/Submission.java | 12 +++++ .../codehive/model/entity/TestCase.java | 11 +++++ .../codehive/model/enums/ComparatorType.java | 7 +++ .../codehive/model/enums/ExecutionStatus.java | 10 +++++ .../github/codehive/model/enums/Language.java | 5 +++ .../codehive/utils/ObjectKeyBuilder.java | 19 ++++++++ codehive-testing-files/programs/1/Main.java | 44 +++++++++++++++++++ codehive-testing-files/testcases/1/tc1.in | 3 ++ codehive-testing-files/testcases/1/tc1.out | 1 + codehive-testing-files/testcases/1/tc2.in | 3 ++ codehive-testing-files/testcases/1/tc2.out | 1 + codehive-testing-files/testcases/1/tc3.in | 3 ++ codehive-testing-files/testcases/1/tc3.out | 1 + codehive-testing-files/testcases/1/tc4.in | 3 ++ codehive-testing-files/testcases/1/tc4.out | 1 + codehive-testing-files/testcases/1/tc5.in | 3 ++ codehive-testing-files/testcases/1/tc5.out | 1 + 20 files changed, 182 insertions(+) create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/entity/Assignment.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/entity/Execution.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/entity/Submission.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/entity/TestCase.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/enums/ComparatorType.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/enums/ExecutionStatus.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/enums/Language.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java create mode 100644 codehive-testing-files/programs/1/Main.java create mode 100644 codehive-testing-files/testcases/1/tc1.in create mode 100644 codehive-testing-files/testcases/1/tc1.out create mode 100644 codehive-testing-files/testcases/1/tc2.in create mode 100644 codehive-testing-files/testcases/1/tc2.out create mode 100644 codehive-testing-files/testcases/1/tc3.in create mode 100644 codehive-testing-files/testcases/1/tc3.out create mode 100644 codehive-testing-files/testcases/1/tc4.in create mode 100644 codehive-testing-files/testcases/1/tc4.out create mode 100644 codehive-testing-files/testcases/1/tc5.in create mode 100644 codehive-testing-files/testcases/1/tc5.out diff --git a/codehive-backend/docker-compose.yaml b/codehive-backend/docker-compose.yaml index cf6389c..e6c211a 100644 --- a/codehive-backend/docker-compose.yaml +++ b/codehive-backend/docker-compose.yaml @@ -18,5 +18,19 @@ services: - '5672:5672' - '15672:15672' + minio: + image: 'minio/minio:RELEASE.2025-09-07T16-13-09Z-cpuv1' + container_name: 'minio' + command: 'server /data --console-address ":9001"' + ports: + - '9000:9000' + - '9001:9001' + environment: + - 'MINIO_ROOT_USER=${MINIO_ROOT_USER}' + - 'MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}' + volumes: + - 'minio_data:/data' + volumes: db_data: + minio_data: \ No newline at end of file diff --git a/codehive-backend/src/main/java/com/github/codehive/model/entity/Assignment.java b/codehive-backend/src/main/java/com/github/codehive/model/entity/Assignment.java new file mode 100644 index 0000000..f65b082 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/entity/Assignment.java @@ -0,0 +1,25 @@ +package com.github.codehive.model.entity; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.cglib.core.Local; + +import com.github.codehive.model.enums.ComparatorType; + +public class Assignment { + private Long id; + private String title; + private String description; + private List constraints; + private List hints; + private List tags; + private Integer timeLimitMs; + private Integer memoryLimitMb; + private ComparatorType comparatorType; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private LocalDateTime dueDate; + + +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/entity/Execution.java b/codehive-backend/src/main/java/com/github/codehive/model/entity/Execution.java new file mode 100644 index 0000000..a754917 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/entity/Execution.java @@ -0,0 +1,15 @@ +package com.github.codehive.model.entity; + +import java.time.LocalDateTime; + +import ch.qos.logback.classic.spi.Configurator.ExecutionStatus; + +public class Execution { + private Long id; + private Submission submission; + private ExecutionStatus status; + private Long timeMs; + private Long memoryMb; + private Boolean isOutdated; + private LocalDateTime createdAt; +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/entity/Submission.java b/codehive-backend/src/main/java/com/github/codehive/model/entity/Submission.java new file mode 100644 index 0000000..dcffc0f --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/entity/Submission.java @@ -0,0 +1,12 @@ +package com.github.codehive.model.entity; + +import java.time.LocalDateTime; + +import com.github.codehive.model.enums.Language; + +public class Submission { + private Long id; + private Assignment assignment; + private Language language; + private LocalDateTime createdAt; +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/entity/TestCase.java b/codehive-backend/src/main/java/com/github/codehive/model/entity/TestCase.java new file mode 100644 index 0000000..b4afcbd --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/entity/TestCase.java @@ -0,0 +1,11 @@ +package com.github.codehive.model.entity; + +import java.time.LocalDateTime; + +public class TestCase { + private Long id; + private Assignment assignment; + private Integer order; + private Boolean isSample; + private LocalDateTime createdAt; +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/enums/ComparatorType.java b/codehive-backend/src/main/java/com/github/codehive/model/enums/ComparatorType.java new file mode 100644 index 0000000..124f8ac --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/enums/ComparatorType.java @@ -0,0 +1,7 @@ +package com.github.codehive.model.enums; + +public enum ComparatorType { + EXACT_MATCH, + FLOATING_POINT, + IGNORE_WHITESPACES +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/enums/ExecutionStatus.java b/codehive-backend/src/main/java/com/github/codehive/model/enums/ExecutionStatus.java new file mode 100644 index 0000000..0c4a848 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/enums/ExecutionStatus.java @@ -0,0 +1,10 @@ +package com.github.codehive.model.enums; + +public enum ExecutionStatus { + TLE, // Time Limit Exceeded + MLE, // Memory Limit Exceeded + RTE, // Runtime Error + CE, // Compilation Error + WA, // Wrong Answer + AC // Accepted +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/enums/Language.java b/codehive-backend/src/main/java/com/github/codehive/model/enums/Language.java new file mode 100644 index 0000000..623da92 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/enums/Language.java @@ -0,0 +1,5 @@ +package com.github.codehive.model.enums; + +public enum Language { + C, CPP, JAVA, PYTHON +} diff --git a/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java b/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java new file mode 100644 index 0000000..7e9403d --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java @@ -0,0 +1,19 @@ +package com.github.codehive.utils; + +public class ObjectKeyBuilder { + public static String testCaseInput(Long assignmentId, Long testCaseId) { + return String.format("test-suites/assignments/%d/tc-%d/tc%d.in", assignmentId, testCaseId, testCaseId); + } + + public static String testCaseOutput(Long assignmentId, Long testCaseId) { + return String.format("test-suites/assignments/%d/tc-%d/tc%d.out", assignmentId, testCaseId, testCaseId); + } + + public static String submissionSourceCode(Long assignmentId, Long submissionId, String fileExtension, Long groupId) { + return String.format("submissions/groups/%d/assignments/%d/submission-%d/Main.%s", groupId, assignmentId, submissionId, fileExtension); + } + + public static String executionOutput(Long submissionId, Long executionId, Long groupId, String fileExtension) { + return String.format("executions/groups/%d/assignments/%d/execution-%d/output.%s", groupId, submissionId, executionId, fileExtension); + } +} diff --git a/codehive-testing-files/programs/1/Main.java b/codehive-testing-files/programs/1/Main.java new file mode 100644 index 0000000..095c416 --- /dev/null +++ b/codehive-testing-files/programs/1/Main.java @@ -0,0 +1,44 @@ +import java.util.*; + +public class Main { + + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + + if (!sc.hasNextInt()) { + sc.close(); + return; + } + + int n = sc.nextInt(); + int[] nums = new int[n]; + + for (int i = 0; i < n; i++) { + nums[i] = sc.nextInt(); + } + + int k = sc.nextInt(); + + sc.close(); + + if (k > n || k <= 0) { + System.out.println(0); + return; + } + + int windowSum = 0; + for (int i = 0; i < k; i++) { + windowSum += nums[i]; + } + + int maxSum = windowSum; + + for (int i = k; i < n; i++) { + windowSum += nums[i] - nums[i - k]; + maxSum = Math.max(maxSum, windowSum); + } + + System.out.println(maxSum); + + } +} diff --git a/codehive-testing-files/testcases/1/tc1.in b/codehive-testing-files/testcases/1/tc1.in new file mode 100644 index 0000000..053ed15 --- /dev/null +++ b/codehive-testing-files/testcases/1/tc1.in @@ -0,0 +1,3 @@ +5 +1 2 3 4 5 +2 diff --git a/codehive-testing-files/testcases/1/tc1.out b/codehive-testing-files/testcases/1/tc1.out new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/codehive-testing-files/testcases/1/tc1.out @@ -0,0 +1 @@ +9 diff --git a/codehive-testing-files/testcases/1/tc2.in b/codehive-testing-files/testcases/1/tc2.in new file mode 100644 index 0000000..de1bafd --- /dev/null +++ b/codehive-testing-files/testcases/1/tc2.in @@ -0,0 +1,3 @@ +8 +2 1 5 1 3 2 6 2 +3 diff --git a/codehive-testing-files/testcases/1/tc2.out b/codehive-testing-files/testcases/1/tc2.out new file mode 100644 index 0000000..b4de394 --- /dev/null +++ b/codehive-testing-files/testcases/1/tc2.out @@ -0,0 +1 @@ +11 diff --git a/codehive-testing-files/testcases/1/tc3.in b/codehive-testing-files/testcases/1/tc3.in new file mode 100644 index 0000000..91709ef --- /dev/null +++ b/codehive-testing-files/testcases/1/tc3.in @@ -0,0 +1,3 @@ +6 +-1 -2 -3 -4 -5 -6 +2 diff --git a/codehive-testing-files/testcases/1/tc3.out b/codehive-testing-files/testcases/1/tc3.out new file mode 100644 index 0000000..a83d1d5 --- /dev/null +++ b/codehive-testing-files/testcases/1/tc3.out @@ -0,0 +1 @@ +-3 diff --git a/codehive-testing-files/testcases/1/tc4.in b/codehive-testing-files/testcases/1/tc4.in new file mode 100644 index 0000000..f7b449c --- /dev/null +++ b/codehive-testing-files/testcases/1/tc4.in @@ -0,0 +1,3 @@ +10 +4 2 10 3 8 1 5 9 6 7 +4 diff --git a/codehive-testing-files/testcases/1/tc4.out b/codehive-testing-files/testcases/1/tc4.out new file mode 100644 index 0000000..978b4e8 --- /dev/null +++ b/codehive-testing-files/testcases/1/tc4.out @@ -0,0 +1 @@ +26 \ No newline at end of file diff --git a/codehive-testing-files/testcases/1/tc5.in b/codehive-testing-files/testcases/1/tc5.in new file mode 100644 index 0000000..7a4db63 --- /dev/null +++ b/codehive-testing-files/testcases/1/tc5.in @@ -0,0 +1,3 @@ +3 +5 5 5 +1 diff --git a/codehive-testing-files/testcases/1/tc5.out b/codehive-testing-files/testcases/1/tc5.out new file mode 100644 index 0000000..7ed6ff8 --- /dev/null +++ b/codehive-testing-files/testcases/1/tc5.out @@ -0,0 +1 @@ +5 From 251c90efcd417ce36368792c662084bb41c11796 Mon Sep 17 00:00:00 2001 From: IrminDev Date: Tue, 20 Jan 2026 23:47:35 -0600 Subject: [PATCH 04/32] Tunned the execution logic to allow executions without submitting the code --- .../java/com/github/codehive/model/entity/Execution.java | 7 +++++-- .../github/codehive/model/entity/ReferenceSolution.java | 9 +++++++++ .../com/github/codehive/model/enums/ExecutionType.java | 6 ++++++ .../java/com/github/codehive/utils/ObjectKeyBuilder.java | 8 ++++++++ 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/entity/ReferenceSolution.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/enums/ExecutionType.java diff --git a/codehive-backend/src/main/java/com/github/codehive/model/entity/Execution.java b/codehive-backend/src/main/java/com/github/codehive/model/entity/Execution.java index a754917..d2f5818 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/entity/Execution.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/entity/Execution.java @@ -2,11 +2,14 @@ import java.time.LocalDateTime; -import ch.qos.logback.classic.spi.Configurator.ExecutionStatus; +import com.github.codehive.model.enums.ExecutionStatus; +import com.github.codehive.model.enums.ExecutionType; + public class Execution { private Long id; - private Submission submission; + private Submission submission; // NULLABLE + private ExecutionType executionType; private ExecutionStatus status; private Long timeMs; private Long memoryMb; diff --git a/codehive-backend/src/main/java/com/github/codehive/model/entity/ReferenceSolution.java b/codehive-backend/src/main/java/com/github/codehive/model/entity/ReferenceSolution.java new file mode 100644 index 0000000..9586dad --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/entity/ReferenceSolution.java @@ -0,0 +1,9 @@ +package com.github.codehive.model.entity; + +import com.github.codehive.model.enums.Language; + +public class ReferenceSolution { + private Long id; + private Assignment assignment; + private Language language; +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/enums/ExecutionType.java b/codehive-backend/src/main/java/com/github/codehive/model/enums/ExecutionType.java new file mode 100644 index 0000000..bc2355c --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/enums/ExecutionType.java @@ -0,0 +1,6 @@ +package com.github.codehive.model.enums; + +public enum ExecutionType { + PRACTICE, + DEFINITIVE +} diff --git a/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java b/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java index 7e9403d..6212707 100644 --- a/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java +++ b/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java @@ -16,4 +16,12 @@ public static String submissionSourceCode(Long assignmentId, Long submissionId, public static String executionOutput(Long submissionId, Long executionId, Long groupId, String fileExtension) { return String.format("executions/groups/%d/assignments/%d/execution-%d/output.%s", groupId, submissionId, executionId, fileExtension); } + + public static String referenceSolutionSourceCode(Long assignmentId, Long referenceSolutionId, String fileExtension) { + return String.format("reference-solutions/assignments/%d/reference-solution-%d/Main.%s", assignmentId, referenceSolutionId, fileExtension); + } + + public static String executionTestCaseOutput(Long executionId, String fileExtension) { + return String.format("test-execution/execution-%d/output.%s", executionId, fileExtension); + } } From abbe2f9fa033236b06e137d9e37ac0425590fc7c Mon Sep 17 00:00:00 2001 From: IrminDev Date: Wed, 21 Jan 2026 22:55:34 -0600 Subject: [PATCH 05/32] Added base logic for remote code execution and communication using rabbit MQ. Also MinIO was implemented --- AGENTS.md | 349 ++++++++++++++---- codehive-backend/build.gradle.kts | 6 + .../github/codehive/config/MinioConfig.java | 19 + .../github/codehive/config/RabbitConfig.java | 16 + .../model/dto/queue/ExecutionJob.java | 148 ++++++++ .../codehive/model/entity/Assignment.java | 2 - .../codehive/model/enums/ComparatorType.java | 3 +- .../codehive/service/ExecutionProducer.java | 23 ++ .../service/ObjectStorageService.java | 37 ++ .../src/main/resources/application.properties | 45 ++- codehive-worker/build.gradle.kts | 7 +- .../worker/config/DockerClientConfig.java | 26 ++ .../codehive/worker/config/MinioConfig.java | 18 + .../worker/config/RabbitMQConfig.java | 20 +- .../listener/SubmissionListener.java | 65 ++++ .../messaging/model/SubmissionMessage.java | 0 .../worker/model/dto/ExecutionReport.java | 175 +++++++++ .../worker/model/dto/TestCaseResult.java | 92 +++++ .../worker/model/dto/queue/ExecutionJob.java | 147 ++++++++ .../worker/model/enums/ComparatorType.java | 6 + .../worker/model/enums/ExecutionStatus.java | 10 + .../worker/model/enums/ExecutionType.java | 6 + .../codehive/worker/model/enums/Language.java | 5 + .../worker/sandbox/ExecutionResult.java | 121 +++++- .../worker/sandbox/LanguageExecutor.java | 12 +- .../codehive/worker/sandbox/c/CExecutor.java | 244 +++++++++++- .../worker/sandbox/cpp/CPPExecutor.java | 243 +++++++++++- .../factory/LanguageExecutorFactory.java | 18 +- .../worker/sandbox/java/JavaExecutor.java | 243 +++++++++++- .../worker/sandbox/python/PythonExecutor.java | 199 +++++++++- .../worker/service/ExecutionService.java | 5 - .../worker/service/ObjectStorageService.java | 60 +++ .../service/OutputComparatorService.java | 127 +++++++ .../worker/service/TestExecutionService.java | 272 ++++++++++++++ .../worker/util/ObjectKeyBuilder.java | 31 ++ .../src/main/resources/application.properties | 12 + 36 files changed, 2702 insertions(+), 110 deletions(-) create mode 100644 codehive-backend/src/main/java/com/github/codehive/config/MinioConfig.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/config/RabbitConfig.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/dto/queue/ExecutionJob.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/service/ExecutionProducer.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/service/ObjectStorageService.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/config/MinioConfig.java delete mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/messaging/model/SubmissionMessage.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionReport.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/model/dto/TestCaseResult.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/model/dto/queue/ExecutionJob.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/model/enums/ComparatorType.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/model/enums/ExecutionStatus.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/model/enums/ExecutionType.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/model/enums/Language.java delete mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/service/ExecutionService.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/service/ObjectStorageService.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/service/OutputComparatorService.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/util/ObjectKeyBuilder.java diff --git a/AGENTS.md b/AGENTS.md index 4153414..36ea4c5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,10 +40,12 @@ CodeHive/ │ ├── codehive-worker/ # Code execution worker service │ ├── src/main/java/com/github/codehive/worker/ -│ │ ├── config/ # RabbitMQ and Docker client configuration +│ │ ├── config/ # RabbitMQ, Docker, and MinIO configuration │ │ ├── messaging/ -│ │ │ ├── listener/ # RabbitMQ message listeners (SubmissionListener) -│ │ │ └── model/ # Message DTOs (SubmissionMessage) +│ │ │ └── listener/ # RabbitMQ message listeners (SubmissionListener) +│ │ ├── model/ +│ │ │ ├── dto/queue/ # ExecutionJob DTO +│ │ │ └── enums/ # Language, ExecutionStatus, ExecutionType, ComparatorType │ │ ├── sandbox/ # Code execution in isolated Docker containers │ │ │ ├── java/ # JavaExecutor for Java code │ │ │ ├── python/ # PythonExecutor for Python code @@ -51,12 +53,11 @@ CodeHive/ │ │ │ ├── cpp/ # CPPExecutor for C++ code │ │ │ ├── factory/ # LanguageExecutorFactory for strategy pattern │ │ │ ├── ExecutionResult.java -│ │ │ ├── LanguageExecutor.java (interface) -│ │ │ └── SandboxExecutor.java -│ │ ├── service/ # Business logic services (empty scaffolding) -│ │ └── util/ # Utility classes (empty scaffolding) +│ │ │ └── LanguageExecutor.java (interface) +│ │ ├── service/ # ObjectStorageService for MinIO +│ │ └── util/ # Utility classes (ObjectKeyBuilder, TimeoutUtils) │ ├── src/test/java/ # Tests with Testcontainers configuration -│ ├── build.gradle.kts # Spring Boot 4.0.1, RabbitMQ, Docker Java client +│ ├── build.gradle.kts # Spring Boot 4.0.1, RabbitMQ, Docker Java client, MinIO │ └── settings.gradle.kts │ ├── codehive-frontend/ # React Router v7 frontend @@ -388,44 +389,62 @@ class AuthServiceTest { ### Worker Java Patterns #### Package Organization -- **config/**: Spring configuration beans (RabbitMQ, Docker client) +- **config/**: Spring configuration beans (RabbitMQ, Docker client, MinIO) - **messaging/**: RabbitMQ message handling - - **listener/**: `@RabbitListener` components - - **model/**: Message DTOs + - **listener/**: `@RabbitListener` components (SubmissionListener) +- **model/**: Data models + - **dto/queue/**: Message DTOs (ExecutionJob) + - **enums/**: Enums (Language, ExecutionStatus, ExecutionType, ComparatorType) - **sandbox/**: Code execution in Docker containers - **{language}/**: Language-specific executor implementations (java, python, c, cpp) - **factory/**: `LanguageExecutorFactory` for strategy pattern - Core interfaces and result models -- **service/**: Business logic (scaffolding for future implementation) -- **util/**: Utility classes (scaffolding for future implementation) +- **service/**: Business logic (ObjectStorageService for MinIO) +- **util/**: Utility classes (ObjectKeyBuilder, TimeoutUtils) #### Language Executor Pattern **Interface** (`sandbox/LanguageExecutor.java`): ```java public interface LanguageExecutor { - // Execute code in sandboxed Docker container - // Return ExecutionResult with stdout, stderr, exit code, execution time + /** + * Execute code with given constraints + * @param sourceCode The source code input stream from MinIO + * @param testInput The test input stream from MinIO (can be null) + * @param timeLimitMs Time limit in milliseconds + * @param memoryLimitMb Memory limit in megabytes + * @return ExecutionResult with verdict and execution details + */ + ExecutionResult execute(InputStream sourceCode, InputStream testInput, + Long timeLimitMs, Long memoryLimitMb) throws Exception; } ``` **Implementation** (e.g., `sandbox/java/JavaExecutor.java`): ```java -@Component("java") // Bean name matches language identifier +@Component("JAVA") // Bean name matches Language enum public class JavaExecutor implements LanguageExecutor { - // 1. Create Docker container with language-specific image - // 2. Write code to temporary file/volume - // 3. Compile (if needed) and execute with timeout - // 4. Capture output and cleanup container - // 5. Return ExecutionResult + private final DockerClient dockerClient; + private static final String JAVA_IMAGE = "openjdk:21-slim"; + + @Override + public ExecutionResult execute(InputStream sourceCode, InputStream testInput, + Long timeLimitMs, Long memoryLimitMb) { + // 1. Create temp directory and save source/input files + // 2. Compile in Docker container (separate from execution) + // 3. Execute in isolated container with resource limits + // 4. Monitor for TLE, MLE, RTE, CE + // 5. Capture output and cleanup + // 6. Return ExecutionResult with status (TLE/MLE/RTE/CE/AC) + } } ``` **Supported Languages**: -- **Java**: `@Component("java")` - JavaExecutor -- **Python**: `@Component("python")` - PythonExecutor -- **C**: `@Component("c")` - CExecutor -- **C++**: `@Component("cpp")` - CPPExecutor +- **Java**: `@Component("JAVA")` - JavaExecutor (openjdk:21-slim) +- **Python**: `@Component("PYTHON")` - PythonExecutor (python:3.11-slim) +- **C**: `@Component("C")` - CExecutor (gcc:latest) +- **C++**: `@Component("CPP")` - CPPExecutor (gcc:latest with g++) **Factory Pattern** (`sandbox/factory/LanguageExecutorFactory.java`): ```java @@ -438,48 +457,208 @@ public class LanguageExecutorFactory { this.executors = executors; } - public LanguageExecutor getExecutor(String language) { - return executors.get(language); // "java", "python", "c", "cpp" + public LanguageExecutor getExecutor(Language language) { + return executors.get(language.name()); // "JAVA", "PYTHON", "C", "CPP" } } ``` +**Execution Result**: +```java +public class ExecutionResult { + private ExecutionStatus status; // TLE, MLE, RTE, CE, WA, AC (enum) + private String output; // stdout + private String errorOutput; // stderr + private Long executionTimeMs; // Execution time + private Long memoryUsedKb; // Memory used (best effort) + private Integer exitCode; // Process exit code + private String compilationError; // Compilation errors + + // Factory methods: compilationError(), runtimeError(), + // timeLimitExceeded(), memoryLimitExceeded(), success() +} +``` + +**Verdict Detection**: +- **CE (Compilation Error)**: Non-zero exit code during compilation phase +- **TLE (Time Limit Exceeded)**: Execution exceeds `timeLimitMs` (detected via Future timeout) +- **MLE (Memory Limit Exceeded)**: Container killed by Docker (exit code 137) +- **RTE (Runtime Error)**: Non-zero exit code during execution +- **AC (Accepted)**: Exit code 0 with successful execution (TODO: output comparison for AC/WA) + #### Messaging Pattern -**Message DTO** (`messaging/model/SubmissionMessage.java`): +**Message DTO** (`model/dto/queue/ExecutionJob.java`): ```java -// Currently empty scaffolding - will contain: -// - submissionId -// - userId -// - language (java, python, c, cpp) -// - sourceCode -// - testCases (optional) -// - timeout +public class ExecutionJob { + private Long id; // Execution ID + private String source; // MinIO path to source code + private String reference; // MinIO path to reference solution + private Language language; // Student code language: JAVA, PYTHON, C, CPP + private Language referenceLanguage; // Reference solution language + private ExecutionType executionType; // PRACTICE or DEFINITIVE + private List testCases; // Inline test inputs (PRACTICE mode) + private String outputPath; // MinIO path to store execution report JSON + private Long timeLimitMs; // Time limit in milliseconds + private Long memoryLimitMb; // Memory limit in megabytes + private Integer numTests; // Number of test cases (DEFINITIVE mode) + private String testsPath; // Base path for test files (DEFINITIVE mode) + private ComparatorType comparatorType; // EXACT_MATCH or FLOATING_POINT +} ``` +**MinIO Path Structure**: +- **Test inputs**: `test-suites/assignments/{assignment-id}/tc-{tc-id}/tc-{tc-id}.in` +- **Expected outputs**: `test-suites/assignments/{assignment-id}/tc-{tc-id}/tc-{tc-id}.out` +- **Submissions**: `submissions/groups/{group-id}/assignments/{assignment-id}/submission-{submission-id}/Main.{ext}` +- **Practice executions**: `test-execution/execution-{id}/Main.{ext}` + +**Execution Types**: +- **DEFINITIVE**: Run against stored test cases (1 to numTests) from `testsPath`, compare with `.out` files +- **PRACTICE**: Run against inline test cases, compare with reference solution output + **Listener** (`messaging/listener/SubmissionListener.java`): ```java -// Currently empty scaffolding - will contain: @Component public class SubmissionListener { - // @RabbitListener(queues = "code.submission.queue") - // public void handleSubmission(SubmissionMessage message) { - // 1. Get executor from factory - // 2. Execute code in sandbox - // 3. Send result back to backend via RabbitMQ - // } + private final TestExecutionService testExecutionService; + + @RabbitListener(queues = "${rabbitmq.queue:codehive_queue}") + public void handleExecutionJob(ExecutionJob job) { + // Execute all test cases and generate comprehensive report + ExecutionReport report = testExecutionService.executeJob(job); + + // Report contains: + // - Overall status (AC/WA/TLE/MLE/RTE/CE) + // - Individual test case results + // - Execution statistics + // - Stored in MinIO at job.getOutputPath() + + logger.info("Execution completed: id={}, status={}, passed={}/{}", + job.getId(), report.getOverallStatus(), + report.getPassedTests(), report.getTotalTests()); + + // TODO: Send result back to backend via RabbitMQ result queue + } +} +``` + +**Test Execution Service** (`service/TestExecutionService.java`): +The service orchestrates test execution based on execution type: + +1. **DEFINITIVE Mode**: + - Downloads source code from `job.getSource()` + - For each test case (1 to `job.getNumTests()`): + - Downloads input from `{testsPath}/tc-{i}/tc-{i}.in` + - Downloads expected output from `{testsPath}/tc-{i}/tc-{i}.out` + - Executes student code with test input + - Compares output using `OutputComparatorService` + - Records result (AC/WA/TLE/MLE/RTE) + +2. **PRACTICE Mode**: + - Downloads student source from `job.getSource()` + - Downloads reference solution from `job.getReference()` + - For each inline test case in `job.getTestCases()`: + - Executes reference solution to get expected output + - Executes student code with same input + - Compares outputs using `OutputComparatorService` + - Records result (AC/WA/TLE/MLE/RTE) + +3. **Output Comparison** (`service/OutputComparatorService.java`): + - **EXACT_MATCH**: Trimmed line-by-line comparison + - **FLOATING_POINT**: Token-by-token with epsilon tolerance (1e-9) + +4. **Report Generation** (`model/dto/ExecutionReport.java`): + - Overall status (worst-case verdict) + - Per-test-case results with timing/memory + - Statistics: passed/failed counts, max time/memory + - Uploaded to MinIO as JSON at `job.getOutputPath()` + +**Configuration** (`config/RabbitMQConfig.java`): +```java +@Configuration +public class RabbitMQConfig { + public static final String QUEUE_NAME = System.getProperty("rabbitmq.queue", "codehive_queue"); + + @Bean + Queue executionQueue() { + return new Queue(QUEUE_NAME, true); // Durable queue + } + + @Bean + public MessageConverter jsonMessageConverter() { + return new Jackson2JsonMessageConverter(); // JSON serialization + } +} +``` + +**MinIO Integration** (`service/ObjectStorageService.java`): +```java +@Service +public class ObjectStorageService { + private final MinioClient minioClient; + private final String bucketName = System.getProperty("minio.bucketName", "codehive"); + + public InputStream download(String objectKey) throws Exception { + return minioClient.getObject( + GetObjectArgs.builder() + .bucket(bucketName) + .object(objectKey) + .build() + ); + } + + public void upload(String objectKey, String content) throws Exception { + byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8); + ByteArrayInputStream stream = new ByteArrayInputStream(contentBytes); + + minioClient.putObject( + PutObjectArgs.builder() + .bucket(bucketName) + .object(objectKey) + .stream(stream, contentBytes.length, -1) + .contentType("text/plain") + .build() + ); + } } ``` #### Sandbox Execution Flow -1. **Message arrives** → `SubmissionListener` receives from RabbitMQ queue -2. **Factory dispatch** → `LanguageExecutorFactory.getExecutor(language)` -3. **Container creation** → Executor creates isolated Docker container -4. **Code execution** → Run code with timeout and resource limits -5. **Result collection** → Capture stdout/stderr, exit code, timing -6. **Cleanup** → Remove container, temporary files -7. **Result publishing** → Send `ExecutionResult` back to backend queue +1. **Message arrives** → `SubmissionListener` receives `ExecutionJob` from RabbitMQ queue (`codehive_queue`) +2. **Download files** → `ObjectStorageService` downloads source code and test input from MinIO +3. **Factory dispatch** → `LanguageExecutorFactory.getExecutor(language)` returns appropriate executor +4. **Temp file creation** → Executor creates temp directory and saves source/input files +5. **Compilation** (if needed) → Separate Docker container compiles code (Java, C, C++) + - Exit code != 0 → Return CE (Compilation Error) +6. **Container creation** → Executor creates isolated Docker container with: + - Language-specific image (openjdk, python, gcc) + - Memory limit (`--memory`, `--memory-swap`) + - CPU limit (`--cpu-quota`) + - Network isolation (`--network=none`) + - Volume mount for source/input files +7. **Code execution** → Run code with timeout monitoring via `Future.get(timeout)` + - Timeout → Return TLE (Time Limit Exceeded) + - Exit code 137 → Return MLE (Memory Limit Exceeded) + - Exit code != 0 → Return RTE (Runtime Error) + - Exit code 0 → Return AC (Accepted) or WA (Wrong Answer, TODO) +8. **Result collection** → Capture stdout, stderr, exit code, execution time +9. **Cleanup** → Remove Docker container, delete temp files +10. **Result publishing** → TODO: Send `ExecutionResult` back to backend via RabbitMQ + +**Docker Images**: +- Java: `openjdk:21-slim` (compilation: `javac`, execution: `java`) +- Python: `python:3.11-slim` (execution: `python`, syntax errors detected as CE) +- C: `gcc:latest` (compilation: `gcc -o program main.c -lm`, execution: `./program`) +- C++: `gcc:latest` (compilation: `g++ -o program main.cpp -std=c++17 -lm`, execution: `./program`) + +**Resource Limits**: +- Default time limit: 5000ms (5 seconds) +- Default memory limit: 256MB +- CPU limit: 1 CPU (100000 quota) +- Network: Disabled (`--network=none`) +- Compilation memory: 512MB (fixed) ### Frontend TypeScript/React Patterns @@ -992,13 +1171,36 @@ void login_WithWrongPassword_ThrowsIncorrectCredentialsException() { ### Infrastructure -1. **RabbitMQ Integration**: Currently in development on `feature/rabbitMQ` branch. Worker service will consume code execution jobs from RabbitMQ queue (`code.submission.queue`). +1. **RabbitMQ Integration**: ✅ **IMPLEMENTED** - Worker service consumes code execution jobs from RabbitMQ queue (`codehive_queue`). Backend sends `ExecutionJob` messages, worker processes them and returns results (result publishing TODO). + +2. **MinIO Integration**: ✅ **IMPLEMENTED** - Backend and worker use MinIO for object storage. Source code and test inputs are stored as files in MinIO bucket (`codehive`), referenced by object keys in `ExecutionJob`. + +3. **Docker Networking**: Backend, worker, PostgreSQL, RabbitMQ, and MinIO communicate via Docker Compose network. Worker accesses host Docker daemon via socket mount (`/var/run/docker.sock`) to create isolated execution containers. -2. **Docker Networking**: Backend, worker, PostgreSQL, and RabbitMQ communicate via Docker Compose network. Check `docker-compose.yaml` for service names. Worker accesses host Docker daemon via socket mount. +4. **Port Conflicts**: Ensure ports are available: + - 5432: PostgreSQL + - 5672/15672: RabbitMQ (AMQP/Management UI) + - 9000/9001: MinIO (API/Console) + - 8080: Backend API + - 3000/5173: Frontend dev server -3. **Port Conflicts**: Ensure ports 5432 (PostgreSQL), 5672/15672 (RabbitMQ), 8080 (backend), 3000/5173 (frontend) are available. +5. **Worker Deployment**: In production, worker should run on separate machines with Docker installed. Use Docker-in-Docker or bind mount Docker socket with caution (security implications). Ensure MinIO and RabbitMQ are accessible from worker. -4. **Worker Deployment**: In production, worker should run on separate machines with Docker installed. Use Docker-in-Docker or bind mount Docker socket with caution (security implications). +6. **Language Executors**: ✅ **IMPLEMENTED** - All four language executors (Java, Python, C, C++) are fully implemented with: + - Docker-based sandboxing + - Resource limits (memory, CPU, network isolation) + - Timeout detection (TLE) + - Memory limit detection (MLE) + - Compilation error detection (CE) + - Runtime error detection (RTE) + - Output comparison for AC/WA verification (EXACT_MATCH and FLOATING_POINT modes) + +7. **Test Execution Pipeline**: ✅ **IMPLEMENTED** - Complete orchestration of test case execution: + - DEFINITIVE mode: Batch execution against stored test cases from MinIO + - PRACTICE mode: Dynamic execution against inline test cases with reference solution comparison + - Per-test-case result tracking with detailed feedback + - Comprehensive execution reports with statistics (passed/failed, timing, memory) + - JSON report upload to MinIO for backend consumption --- @@ -1024,11 +1226,20 @@ void login_WithWrongPassword_ThrowsIncorrectCredentialsException() { **Solution**: Start RabbitMQ via backend's `docker-compose up -d rabbitmq` **Issue**: Container creation fails -**Solution**: Pull required images manually: `docker pull openjdk:21`, `docker pull python:3.11`, etc. +**Solution**: Pull required images manually: `docker pull openjdk:21-slim`, `docker pull python:3.11-slim`, `docker pull gcc:latest` **Issue**: Tests fail with Testcontainers errors **Solution**: Ensure Docker is accessible for tests. Check Docker socket permissions. +**Issue**: MinIO connection errors +**Solution**: Ensure MinIO is running and accessible. Check `minio.url`, `minio.accessKey`, `minio.secretKey` in environment variables or system properties. + +**Issue**: Execution jobs not being consumed +**Solution**: Check RabbitMQ queue name matches between backend and worker (`codehive_queue`). Verify worker is connected to RabbitMQ via management UI (http://localhost:15672). + +**Issue**: Language executor not found +**Solution**: Verify bean names match enum values: `@Component("JAVA")`, `@Component("PYTHON")`, `@Component("C")`, `@Component("CPP")` (all uppercase). + ### Frontend Can't Connect to Backend **Issue**: CORS errors in browser console @@ -1057,6 +1268,7 @@ void login_WithWrongPassword_ThrowsIncorrectCredentialsException() { - **Spring Boot Docs**: https://spring.io/projects/spring-boot - **Spring AMQP (RabbitMQ)**: https://spring.io/projects/spring-amqp - **Docker Java Client**: https://github.com/docker-java/docker-java +- **MinIO Java SDK**: https://min.io/docs/minio/linux/developers/java/minio-java.html - **React Router v7 Docs**: https://reactrouter.com/ - **Bucket4j (Rate Limiting)**: https://bucket4j.com/ - **Jacoco (Coverage)**: https://www.jacoco.org/jacoco/ @@ -1067,7 +1279,7 @@ void login_WithWrongPassword_ThrowsIncorrectCredentialsException() { ## Future Enhancements -1. **Code Execution**: Complete worker service integration with sandboxed Docker containers (IN PROGRESS) +1. **Result Publishing**: ✅ **NEXT** - Send `ExecutionReport` back to backend via RabbitMQ result queue 2. **Real-time Collaboration**: WebSocket support for live code editing 3. **Group Management**: Teacher/student group creation and assignment submission 4. **File Upload**: Support for uploading code files and project archives @@ -1076,15 +1288,26 @@ void login_WithWrongPassword_ThrowsIncorrectCredentialsException() { 7. **Email Templates**: HTML email templates for password reset and notifications 8. **Admin Dashboard**: Frontend admin panel for user management 9. **API Versioning**: Add `/v1/` prefix to API routes for future compatibility -10. **Monitoring**: Add Actuator endpoints and Prometheus metrics -11. **Multi-file Support**: Worker support for projects with multiple source files -12. **Test Case Validation**: Automated test case execution and grading -13. **Language Extensions**: Add support for Go, Rust, JavaScript, TypeScript -14. **Execution History**: Store and display past code execution results +10. **Monitoring**: Add Actuator endpoints and Prometheus metrics for worker execution metrics +11. **Multi-file Support**: Worker support for projects with multiple source files and complex build systems +12. **Language Extensions**: Add support for Go, Rust, JavaScript, TypeScript, Ruby +13. **Execution History**: Store and display past code execution results with analytics +14. **Security Hardening**: Add seccomp profiles, AppArmor/SELinux policies for worker containers +15. **Horizontal Scaling**: Support multiple worker instances with load balancing --- -**Last Updated**: 2025-01-15 +**Last Updated**: 2026-01-21 **Current Branch**: `feature/rabbitMQ` -**Project Status**: Active Development (Alpha) -**Worker Status**: Architecture defined, implementation scaffolded (config stubs, empty listeners, language executors with bean naming) +**Project Status**: Active Development (Alpha) +**Worker Status**: ✅ **FULLY OPERATIONAL** - Complete test execution pipeline implemented: +- RabbitMQ integration with comprehensive `ExecutionJob` model +- MinIO integration for source code, test cases, and execution reports +- All 4 language executors (Java, Python, C, C++) with Docker sandboxing +- DEFINITIVE mode: Multi-test execution with stored test cases +- PRACTICE mode: Reference solution comparison +- Output comparison service with EXACT_MATCH and FLOATING_POINT modes +- Comprehensive execution reports with per-test-case results and statistics +- TLE/MLE/RTE/CE/AC/WA verdict detection +- Report storage in MinIO as JSON +- Next: Result publishing to backend via RabbitMQ result queue diff --git a/codehive-backend/build.gradle.kts b/codehive-backend/build.gradle.kts index 5dbdfe5..e3de5b8 100644 --- a/codehive-backend/build.gradle.kts +++ b/codehive-backend/build.gradle.kts @@ -54,6 +54,12 @@ dependencies { testImplementation("org.assertj:assertj-core") testRuntimeOnly("org.junit.platform:junit-platform-launcher") testRuntimeOnly("com.h2database:h2") + + // Minio + implementation("io.minio:minio:8.6.0") + + // RabbitMQ + implementation("org.springframework.boot:spring-boot-starter-amqp") } tasks.withType { diff --git a/codehive-backend/src/main/java/com/github/codehive/config/MinioConfig.java b/codehive-backend/src/main/java/com/github/codehive/config/MinioConfig.java new file mode 100644 index 0000000..4264858 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/config/MinioConfig.java @@ -0,0 +1,19 @@ +package com.github.codehive.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.minio.MinioClient; + +@Configuration +public class MinioConfig { + @Bean + MinioClient minioClient() { + return MinioClient.builder() + .endpoint(System.getProperty("minio.url", "http://exampleurl.com:9000")) + .credentials( + System.getProperty("minio.accessKey", "accesskey"), + System.getProperty("minio.secretKey", "secretkey")) + .build(); + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/config/RabbitConfig.java b/codehive-backend/src/main/java/com/github/codehive/config/RabbitConfig.java new file mode 100644 index 0000000..48723c1 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/config/RabbitConfig.java @@ -0,0 +1,16 @@ +package com.github.codehive.config; + +import org.springframework.amqp.core.Queue; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RabbitConfig { + public static final String QUEUE_NAME = System.getProperty("rabbitmq.queue", "codehive_queue"); + + @Bean + Queue executionQueue(){ + return new Queue(QUEUE_NAME, true); + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/ExecutionJob.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/ExecutionJob.java new file mode 100644 index 0000000..ff199d7 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/ExecutionJob.java @@ -0,0 +1,148 @@ +package com.github.codehive.model.dto.queue; + +import java.util.List; + +import com.github.codehive.model.enums.ComparatorType; +import com.github.codehive.model.enums.ExecutionType; +import com.github.codehive.model.enums.Language; + + +public class ExecutionJob { + private Long id; + private String source; + private String reference; + private Language language; + private ExecutionType executionType; + private List testCases; + private String outputPath; + private Long timeLimitMs; + private Long memoryLimitMb; + private Integer numTests; + private Language referenceLanguage; + private String testsPath; + private ComparatorType comparatorType; + + public ExecutionJob() { + } + + public ExecutionJob(Long id, String source, String reference, Language language, + ExecutionType executionType, List testCases, + Long timeLimitMs, Long memoryLimitMb, ComparatorType comparatorType, String outputPath, Integer numTests, String testsPath, Language referenceLanguage) { + this.id = id; + this.source = source; + this.reference = reference; + this.language = language; + this.executionType = executionType; + this.testCases = testCases; + this.timeLimitMs = timeLimitMs; + this.memoryLimitMb = memoryLimitMb; + this.comparatorType = comparatorType; + this.outputPath = outputPath; + this.numTests = numTests; + this.testsPath = testsPath; + this.referenceLanguage = referenceLanguage; + } + + public Language getReferenceLanguage() { + return referenceLanguage; + } + + public void setReferenceLanguage(Language referenceLanguage) { + this.referenceLanguage = referenceLanguage; + } + + public String getTestsPath() { + return testsPath; + } + + public void setTestsPath(String testsPath) { + this.testsPath = testsPath; + } + + public Integer getNumTests() { + return numTests; + } + public void setNumTests(Integer numTests) { + this.numTests = numTests; + } + + public String getOutputPath() { + return outputPath; + } + + public void setOutputPath(String outputPath) { + this.outputPath = outputPath; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public String getReference() { + return reference; + } + + public void setReference(String reference) { + this.reference = reference; + } + + public Language getLanguage() { + return language; + } + + public void setLanguage(Language language) { + this.language = language; + } + + public ExecutionType getExecutionType() { + return executionType; + } + + public void setExecutionType(ExecutionType executionType) { + this.executionType = executionType; + } + + public List getTestCases() { + return testCases; + } + + public void setTestCases(List testCases) { + this.testCases = testCases; + } + + public Long getTimeLimitMs() { + return timeLimitMs; + } + + public void setTimeLimitMs(Long timeLimitMs) { + this.timeLimitMs = timeLimitMs; + } + + public Long getMemoryLimitMb() { + return memoryLimitMb; + } + + public void setMemoryLimitMb(Long memoryLimitMb) { + this.memoryLimitMb = memoryLimitMb; + } + + public ComparatorType getComparatorType() { + return comparatorType; + } + + public void setComparatorType(ComparatorType comparatorType) { + this.comparatorType = comparatorType; + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/entity/Assignment.java b/codehive-backend/src/main/java/com/github/codehive/model/entity/Assignment.java index f65b082..973b79f 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/entity/Assignment.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/entity/Assignment.java @@ -3,8 +3,6 @@ import java.time.LocalDateTime; import java.util.List; -import org.springframework.cglib.core.Local; - import com.github.codehive.model.enums.ComparatorType; public class Assignment { diff --git a/codehive-backend/src/main/java/com/github/codehive/model/enums/ComparatorType.java b/codehive-backend/src/main/java/com/github/codehive/model/enums/ComparatorType.java index 124f8ac..a978681 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/enums/ComparatorType.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/enums/ComparatorType.java @@ -2,6 +2,5 @@ public enum ComparatorType { EXACT_MATCH, - FLOATING_POINT, - IGNORE_WHITESPACES + FLOATING_POINT } diff --git a/codehive-backend/src/main/java/com/github/codehive/service/ExecutionProducer.java b/codehive-backend/src/main/java/com/github/codehive/service/ExecutionProducer.java new file mode 100644 index 0000000..019eba2 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/service/ExecutionProducer.java @@ -0,0 +1,23 @@ +package com.github.codehive.service; + +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.stereotype.Service; + +import com.github.codehive.config.RabbitConfig; +import com.github.codehive.model.dto.queue.ExecutionJob; + +@Service +public class ExecutionProducer { + private final RabbitTemplate rabbitTemplate; + + public ExecutionProducer(RabbitTemplate rabbitTemplate) { + this.rabbitTemplate = rabbitTemplate; + } + + public void sendExecutionRequest(ExecutionJob executionJob) { + rabbitTemplate.convertAndSend( + RabbitConfig.QUEUE_NAME, + executionJob + ); + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/service/ObjectStorageService.java b/codehive-backend/src/main/java/com/github/codehive/service/ObjectStorageService.java new file mode 100644 index 0000000..9120088 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/service/ObjectStorageService.java @@ -0,0 +1,37 @@ +package com.github.codehive.service; + +import java.io.InputStream; + +import org.springframework.stereotype.Service; + +import io.minio.MinioClient; + +@Service +public class ObjectStorageService { + private final MinioClient minioClient; + private final String bucketName = System.getProperty("minio.bucketName", "codehive"); + + public ObjectStorageService(MinioClient minioClient) { + this.minioClient = minioClient; + } + + public void upload(String objectKey, InputStream data, long size, String contentType) throws Exception { + minioClient.putObject( + io.minio.PutObjectArgs.builder() + .bucket(bucketName) + .object(objectKey) + .stream(data, size, -1) + .contentType(contentType) + .build() + ); + } + + public InputStream download(String objectKey) throws Exception { + return minioClient.getObject( + io.minio.GetObjectArgs.builder() + .bucket(bucketName) + .object(objectKey) + .build() + ); + } +} diff --git a/codehive-backend/src/main/resources/application.properties b/codehive-backend/src/main/resources/application.properties index 808b485..21d4ba6 100644 --- a/codehive-backend/src/main/resources/application.properties +++ b/codehive-backend/src/main/resources/application.properties @@ -2,9 +2,9 @@ spring.config.import=optional:file:.env[.properties] spring.application.name=codehive -spring.datasource.url=${DATABASE_URL:jdbc:postgresql://localhost:5432/codehive} -spring.datasource.username=${DATABASE_USERNAME:codehive} -spring.datasource.password=${DATABASE_PASSWORD:codehive} +spring.datasource.url=${DATABASE_URL} +spring.datasource.username=${DATABASE_USERNAME} +spring.datasource.password=${DATABASE_PASSWORD} spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=update @@ -16,23 +16,30 @@ server.port=8080 # JWT Configuration # The secret must be a Base64-encoded key of at least 256 bits (32 bytes) for HS256 -security.jwt.secret=${JWT_SECRET:Y29kZWhpdmVTZWNyZXRLZXlGb3JKd3RTaWduaW5nMjU2Qml0c09r} -security.jwt.expiration=${JWT_EXPIRATION:36000000} +security.jwt.secret=${JWT_SECRET} +security.jwt.expiration=${JWT_EXPIRATION} # Java Mail Configuration -spring.mail.host=${MAIL_HOST:smtp.gmail.com} -spring.mail.port=${MAIL_PORT:587} -spring.mail.username=${MAIL_USERNAME:codehive@codehive.com} -spring.mail.password=${MAIL_PASSWORD:codehive} -spring.mail.properties.mail.smtp.auth=${MAIL_SMTP_AUTH:true} -spring.mail.properties.mail.smtp.starttls.enable=${MAIL_STARTTLS_ENABLE:true} +spring.mail.host=${MAIL_HOST} +spring.mail.port=${MAIL_PORT} +spring.mail.username=${MAIL_USERNAME} +spring.mail.password=${MAIL_PASSWORD} +spring.mail.properties.mail.smtp.auth=${MAIL_SMTP_AUTH} +spring.mail.properties.mail.smtp.starttls.enable=${MAIL_STARTTLS_ENABLE} # Frontend URL -frontend.url=${FRONTEND_URL:http://localhost:5173} - -# Default Super Admin Configuration -app.admin.email=${ADMIN_EMAIL:admin@codehive.com} -app.admin.password=${ADMIN_PASSWORD} -app.admin.name=${ADMIN_NAME:Super} -app.admin.lastName=${ADMIN_LAST_NAME:Admin} -app.admin.enrollmentNumber=${ADMIN_ENROLLMENT:ADMIN-001} \ No newline at end of file +frontend.url=${FRONTEND_URL} + +# Minio Configuration +minio.url=${MINIO_URL} +minio.accessKey=${MINIO_ACCESS_KEY} +minio.secretKey=${MINIO_SECRET_KEY} +minio.bucketName=${MINIO_BUCKET_NAME} + +# RabbitMQ Configuration +spring.rabbitmq.host=${RABBITMQ_HOST} +spring.rabbitmq.port=${RABBITMQ_PORT} +spring.rabbitmq.username=${RABBITMQ_USERNAME} +spring.rabbitmq.password=${RABBITMQ_PASSWORD} +spring.rabbitmq.queue=${RABBITMQ_QUEUE} + diff --git a/codehive-worker/build.gradle.kts b/codehive-worker/build.gradle.kts index 2cdeaed..11b61ba 100644 --- a/codehive-worker/build.gradle.kts +++ b/codehive-worker/build.gradle.kts @@ -20,12 +20,17 @@ repositories { dependencies { implementation("org.springframework.boot:spring-boot-starter") - implementation("org.springframework.boot:spring-boot-starter-amqp") implementation("com.github.docker-java:docker-java-core:3.7.0") implementation("com.github.docker-java:docker-java-transport-httpclient5:3.7.0") testImplementation("org.springframework.boot:spring-boot-starter-test") testRuntimeOnly("org.junit.platform:junit-platform-launcher") + // Minio + implementation("io.minio:minio:8.6.0") + + // RabbitMQ + implementation("org.springframework.boot:spring-boot-starter-amqp") + // Timeout utility implementation("io.github.resilience4j:resilience4j-timelimiter:2.3.0") } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/config/DockerClientConfig.java b/codehive-worker/src/main/java/com/github/codehive/worker/config/DockerClientConfig.java index da8ffbb..af7477b 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/config/DockerClientConfig.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/config/DockerClientConfig.java @@ -1,5 +1,31 @@ package com.github.codehive.worker.config; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.core.DefaultDockerClientConfig; +import com.github.dockerjava.core.DockerClientImpl; +import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; +import com.github.dockerjava.transport.DockerHttpClient; + +import java.time.Duration; + +@Configuration public class DockerClientConfig { + @Bean + public DockerClient dockerClient() { + com.github.dockerjava.core.DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder() + .withDockerHost("unix:///var/run/docker.sock") + .build(); + + DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder() + .dockerHost(config.getDockerHost()) + .maxConnections(100) + .connectionTimeout(Duration.ofSeconds(30)) + .responseTimeout(Duration.ofSeconds(45)) + .build(); + + return DockerClientImpl.getInstance(config, httpClient); + } } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/config/MinioConfig.java b/codehive-worker/src/main/java/com/github/codehive/worker/config/MinioConfig.java new file mode 100644 index 0000000..05b13a6 --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/config/MinioConfig.java @@ -0,0 +1,18 @@ +package com.github.codehive.worker.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import io.minio.MinioClient; + +@Configuration +public class MinioConfig { + @Bean + MinioClient minioClient() { + return MinioClient.builder() + .endpoint(System.getProperty("minio.url", "http://localhost:9000")) + .credentials( + System.getProperty("minio.accessKey", "minioadmin"), + System.getProperty("minio.secretKey", "minioadmin")) + .build(); + } +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/config/RabbitMQConfig.java b/codehive-worker/src/main/java/com/github/codehive/worker/config/RabbitMQConfig.java index c3a0a18..bb4a2a0 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/config/RabbitMQConfig.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/config/RabbitMQConfig.java @@ -1,5 +1,23 @@ package com.github.codehive.worker.config; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration public class RabbitMQConfig { - + public static final String QUEUE_NAME = System.getProperty("rabbitmq.queue", "codehive_queue"); + + @Bean + Queue executionQueue() { + return new Queue(QUEUE_NAME, true); + } + + @Bean + @SuppressWarnings("removal") + public MessageConverter jsonMessageConverter() { + return new Jackson2JsonMessageConverter(); + } } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/SubmissionListener.java b/codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/SubmissionListener.java index 8f1466b..f553a1d 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/SubmissionListener.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/SubmissionListener.java @@ -1,5 +1,70 @@ package com.github.codehive.worker.messaging.listener; +import com.github.codehive.worker.model.dto.ExecutionReport; +import com.github.codehive.worker.model.dto.queue.ExecutionJob; +import com.github.codehive.worker.service.TestExecutionService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +@Component public class SubmissionListener { + private static final Logger logger = LoggerFactory.getLogger(SubmissionListener.class); + + private final TestExecutionService testExecutionService; + + public SubmissionListener(TestExecutionService testExecutionService) { + this.testExecutionService = testExecutionService; + } + + @RabbitListener(queues = "${rabbitmq.queue:codehive_queue}") + public void handleExecutionJob(ExecutionJob job) { + logger.info("Received execution job: id={}, language={}, type={}", + job.getId(), job.getLanguage(), job.getExecutionType()); + + try { + // Execute the job and get the report + ExecutionReport report = testExecutionService.executeJob(job); + + logger.info("Execution completed: id={}, status={}, passed={}/{}", + job.getId(), report.getOverallStatus(), report.getPassedTests(), report.getTotalTests()); + + // Log detailed results + logExecutionReport(report); + + // TODO: Send result back to backend via RabbitMQ result queue + // This will be implemented when we have a result queue configured + + } catch (Exception e) { + logger.error("Failed to process execution job: id={}", job.getId(), e); + // TODO: Send error result back to backend + } + } + private void logExecutionReport(ExecutionReport report) { + logger.info("=== Execution Report ==="); + logger.info("Execution ID: {}", report.getExecutionId()); + logger.info("Overall Status: {}", report.getOverallStatus()); + logger.info("Tests: {}/{} passed", report.getPassedTests(), report.getTotalTests()); + + if (report.getCompilationError() != null) { + logger.info("Compilation Error: {}", + report.getCompilationError().substring(0, Math.min(200, report.getCompilationError().length()))); + } + + if (report.getTotalExecutionTimeMs() != null) { + logger.info("Total Execution Time: {}ms", report.getTotalExecutionTimeMs()); + } + + if (report.getMaxExecutionTimeMs() != null) { + logger.info("Max Execution Time: {}ms", report.getMaxExecutionTimeMs()); + } + + if (report.getMaxMemoryUsedKb() != null) { + logger.info("Max Memory Used: {}KB", report.getMaxMemoryUsedKb()); + } + + logger.info("========================"); + } } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/messaging/model/SubmissionMessage.java b/codehive-worker/src/main/java/com/github/codehive/worker/messaging/model/SubmissionMessage.java deleted file mode 100644 index e69de29..0000000 diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionReport.java b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionReport.java new file mode 100644 index 0000000..68165fa --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionReport.java @@ -0,0 +1,175 @@ +package com.github.codehive.worker.model.dto; + +import com.github.codehive.worker.model.enums.ExecutionStatus; + +import java.util.ArrayList; +import java.util.List; + +public class ExecutionReport { + private Long executionId; + private ExecutionStatus overallStatus; + private List testCaseResults; + private int totalTests; + private int passedTests; + private int failedTests; + private Long totalExecutionTimeMs; + private Long maxExecutionTimeMs; + private Long maxMemoryUsedKb; + private String compilationError; + + public ExecutionReport() { + this.testCaseResults = new ArrayList<>(); + } + + public ExecutionReport(Long executionId) { + this.executionId = executionId; + this.testCaseResults = new ArrayList<>(); + } + + public void addTestCaseResult(TestCaseResult result) { + this.testCaseResults.add(result); + updateStatistics(result); + } + + private void updateStatistics(TestCaseResult result) { + this.totalTests = testCaseResults.size(); + + if (result.getStatus() == ExecutionStatus.AC) { + this.passedTests++; + } else { + this.failedTests++; + } + + // Update timing stats + if (result.getExecutionTimeMs() != null) { + if (this.totalExecutionTimeMs == null) { + this.totalExecutionTimeMs = 0L; + } + this.totalExecutionTimeMs += result.getExecutionTimeMs(); + + if (this.maxExecutionTimeMs == null || result.getExecutionTimeMs() > this.maxExecutionTimeMs) { + this.maxExecutionTimeMs = result.getExecutionTimeMs(); + } + } + + // Update memory stats + if (result.getMemoryUsedKb() != null) { + if (this.maxMemoryUsedKb == null || result.getMemoryUsedKb() > this.maxMemoryUsedKb) { + this.maxMemoryUsedKb = result.getMemoryUsedKb(); + } + } + } + + public void determineOverallStatus() { + if (this.compilationError != null) { + this.overallStatus = ExecutionStatus.CE; + return; + } + + if (testCaseResults.isEmpty()) { + this.overallStatus = ExecutionStatus.AC; + return; + } + + // Check if all tests passed + boolean allPassed = testCaseResults.stream() + .allMatch(result -> result.getStatus() == ExecutionStatus.AC); + + if (allPassed) { + this.overallStatus = ExecutionStatus.AC; + return; + } + + // Determine the most severe failure + if (testCaseResults.stream().anyMatch(r -> r.getStatus() == ExecutionStatus.TLE)) { + this.overallStatus = ExecutionStatus.TLE; + } else if (testCaseResults.stream().anyMatch(r -> r.getStatus() == ExecutionStatus.MLE)) { + this.overallStatus = ExecutionStatus.MLE; + } else if (testCaseResults.stream().anyMatch(r -> r.getStatus() == ExecutionStatus.RTE)) { + this.overallStatus = ExecutionStatus.RTE; + } else { + this.overallStatus = ExecutionStatus.WA; + } + } + + // Getters and setters + public Long getExecutionId() { + return executionId; + } + + public void setExecutionId(Long executionId) { + this.executionId = executionId; + } + + public ExecutionStatus getOverallStatus() { + return overallStatus; + } + + public void setOverallStatus(ExecutionStatus overallStatus) { + this.overallStatus = overallStatus; + } + + public List getTestCaseResults() { + return testCaseResults; + } + + public void setTestCaseResults(List testCaseResults) { + this.testCaseResults = testCaseResults; + } + + public int getTotalTests() { + return totalTests; + } + + public void setTotalTests(int totalTests) { + this.totalTests = totalTests; + } + + public int getPassedTests() { + return passedTests; + } + + public void setPassedTests(int passedTests) { + this.passedTests = passedTests; + } + + public int getFailedTests() { + return failedTests; + } + + public void setFailedTests(int failedTests) { + this.failedTests = failedTests; + } + + public Long getTotalExecutionTimeMs() { + return totalExecutionTimeMs; + } + + public void setTotalExecutionTimeMs(Long totalExecutionTimeMs) { + this.totalExecutionTimeMs = totalExecutionTimeMs; + } + + public Long getMaxExecutionTimeMs() { + return maxExecutionTimeMs; + } + + public void setMaxExecutionTimeMs(Long maxExecutionTimeMs) { + this.maxExecutionTimeMs = maxExecutionTimeMs; + } + + public Long getMaxMemoryUsedKb() { + return maxMemoryUsedKb; + } + + public void setMaxMemoryUsedKb(Long maxMemoryUsedKb) { + this.maxMemoryUsedKb = maxMemoryUsedKb; + } + + public String getCompilationError() { + return compilationError; + } + + public void setCompilationError(String compilationError) { + this.compilationError = compilationError; + } +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/TestCaseResult.java b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/TestCaseResult.java new file mode 100644 index 0000000..a8b7df3 --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/TestCaseResult.java @@ -0,0 +1,92 @@ +package com.github.codehive.worker.model.dto; + +import com.github.codehive.worker.model.enums.ExecutionStatus; + +public class TestCaseResult { + private int testCaseNumber; + private ExecutionStatus status; + private String output; + private String expectedOutput; + private String errorOutput; + private Long executionTimeMs; + private Long memoryUsedKb; + private String feedback; + + public TestCaseResult() { + } + + public TestCaseResult(int testCaseNumber, ExecutionStatus status, String output, + String expectedOutput, Long executionTimeMs, Long memoryUsedKb) { + this.testCaseNumber = testCaseNumber; + this.status = status; + this.output = output; + this.expectedOutput = expectedOutput; + this.executionTimeMs = executionTimeMs; + this.memoryUsedKb = memoryUsedKb; + } + + // Getters and setters + public int getTestCaseNumber() { + return testCaseNumber; + } + + public void setTestCaseNumber(int testCaseNumber) { + this.testCaseNumber = testCaseNumber; + } + + public ExecutionStatus getStatus() { + return status; + } + + public void setStatus(ExecutionStatus status) { + this.status = status; + } + + public String getOutput() { + return output; + } + + public void setOutput(String output) { + this.output = output; + } + + public String getExpectedOutput() { + return expectedOutput; + } + + public void setExpectedOutput(String expectedOutput) { + this.expectedOutput = expectedOutput; + } + + public String getErrorOutput() { + return errorOutput; + } + + public void setErrorOutput(String errorOutput) { + this.errorOutput = errorOutput; + } + + public Long getExecutionTimeMs() { + return executionTimeMs; + } + + public void setExecutionTimeMs(Long executionTimeMs) { + this.executionTimeMs = executionTimeMs; + } + + public Long getMemoryUsedKb() { + return memoryUsedKb; + } + + public void setMemoryUsedKb(Long memoryUsedKb) { + this.memoryUsedKb = memoryUsedKb; + } + + public String getFeedback() { + return feedback; + } + + public void setFeedback(String feedback) { + this.feedback = feedback; + } +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/queue/ExecutionJob.java b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/queue/ExecutionJob.java new file mode 100644 index 0000000..65ec0e4 --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/queue/ExecutionJob.java @@ -0,0 +1,147 @@ +package com.github.codehive.worker.model.dto.queue; + +import java.util.List; + +import com.github.codehive.worker.model.enums.ComparatorType; +import com.github.codehive.worker.model.enums.ExecutionType; +import com.github.codehive.worker.model.enums.Language; + +public class ExecutionJob { + private Long id; + private String source; + private String reference; + private Language language; + private ExecutionType executionType; + private List testCases; + private String outputPath; + private Long timeLimitMs; + private Long memoryLimitMb; + private Integer numTests; + private Language referenceLanguage; + private String testsPath; + private ComparatorType comparatorType; + + public ExecutionJob() { + } + + public ExecutionJob(Long id, String source, String reference, Language language, + ExecutionType executionType, List testCases, + Long timeLimitMs, Long memoryLimitMb, ComparatorType comparatorType, String outputPath, Integer numTests, String testsPath, Language referenceLanguage) { + this.id = id; + this.source = source; + this.reference = reference; + this.language = language; + this.executionType = executionType; + this.testCases = testCases; + this.timeLimitMs = timeLimitMs; + this.memoryLimitMb = memoryLimitMb; + this.comparatorType = comparatorType; + this.outputPath = outputPath; + this.numTests = numTests; + this.testsPath = testsPath; + this.referenceLanguage = referenceLanguage; + } + + public Language getReferenceLanguage() { + return referenceLanguage; + } + + public void setReferenceLanguage(Language referenceLanguage) { + this.referenceLanguage = referenceLanguage; + } + + public String getTestsPath() { + return testsPath; + } + + public void setTestsPath(String testsPath) { + this.testsPath = testsPath; + } + + public Integer getNumTests() { + return numTests; + } + public void setNumTests(Integer numTests) { + this.numTests = numTests; + } + + public String getOutputPath() { + return outputPath; + } + + public void setOutputPath(String outputPath) { + this.outputPath = outputPath; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public String getReference() { + return reference; + } + + public void setReference(String reference) { + this.reference = reference; + } + + public Language getLanguage() { + return language; + } + + public void setLanguage(Language language) { + this.language = language; + } + + public ExecutionType getExecutionType() { + return executionType; + } + + public void setExecutionType(ExecutionType executionType) { + this.executionType = executionType; + } + + public List getTestCases() { + return testCases; + } + + public void setTestCases(List testCases) { + this.testCases = testCases; + } + + public Long getTimeLimitMs() { + return timeLimitMs; + } + + public void setTimeLimitMs(Long timeLimitMs) { + this.timeLimitMs = timeLimitMs; + } + + public Long getMemoryLimitMb() { + return memoryLimitMb; + } + + public void setMemoryLimitMb(Long memoryLimitMb) { + this.memoryLimitMb = memoryLimitMb; + } + + public ComparatorType getComparatorType() { + return comparatorType; + } + + public void setComparatorType(ComparatorType comparatorType) { + this.comparatorType = comparatorType; + } +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/model/enums/ComparatorType.java b/codehive-worker/src/main/java/com/github/codehive/worker/model/enums/ComparatorType.java new file mode 100644 index 0000000..9670998 --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/model/enums/ComparatorType.java @@ -0,0 +1,6 @@ +package com.github.codehive.worker.model.enums; + +public enum ComparatorType { + EXACT_MATCH, + FLOATING_POINT +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/model/enums/ExecutionStatus.java b/codehive-worker/src/main/java/com/github/codehive/worker/model/enums/ExecutionStatus.java new file mode 100644 index 0000000..2df58b4 --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/model/enums/ExecutionStatus.java @@ -0,0 +1,10 @@ +package com.github.codehive.worker.model.enums; + +public enum ExecutionStatus { + TLE, // Time Limit Exceeded + MLE, // Memory Limit Exceeded + RTE, // Runtime Error + CE, // Compilation Error + WA, // Wrong Answer + AC // Accepted +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/model/enums/ExecutionType.java b/codehive-worker/src/main/java/com/github/codehive/worker/model/enums/ExecutionType.java new file mode 100644 index 0000000..590f101 --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/model/enums/ExecutionType.java @@ -0,0 +1,6 @@ +package com.github.codehive.worker.model.enums; + +public enum ExecutionType { + PRACTICE, + DEFINITIVE +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/model/enums/Language.java b/codehive-worker/src/main/java/com/github/codehive/worker/model/enums/Language.java new file mode 100644 index 0000000..e6f2c58 --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/model/enums/Language.java @@ -0,0 +1,5 @@ +package com.github.codehive.worker.model.enums; + +public enum Language { + C, CPP, JAVA, PYTHON +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/ExecutionResult.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/ExecutionResult.java index c7075ff..1c1802a 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/ExecutionResult.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/ExecutionResult.java @@ -1,5 +1,124 @@ package com.github.codehive.worker.sandbox; +import com.github.codehive.worker.model.enums.ExecutionStatus; + public class ExecutionResult { - + private ExecutionStatus status; // TLE, MLE, RTE, CE, WA, AC + private String output; + private String errorOutput; + private Long executionTimeMs; + private Long memoryUsedKb; + private Integer exitCode; + private String compilationError; + + public ExecutionResult() { + } + + public ExecutionResult(ExecutionStatus status, String output, String errorOutput, + Long executionTimeMs, Long memoryUsedKb, Integer exitCode) { + this.status = status; + this.output = output; + this.errorOutput = errorOutput; + this.executionTimeMs = executionTimeMs; + this.memoryUsedKb = memoryUsedKb; + this.exitCode = exitCode; + } + + // Factory methods for different verdicts + public static ExecutionResult compilationError(String error) { + ExecutionResult result = new ExecutionResult(); + result.status = ExecutionStatus.CE; + result.compilationError = error; + return result; + } + + public static ExecutionResult runtimeError(String errorOutput, Integer exitCode, Long executionTime) { + ExecutionResult result = new ExecutionResult(); + result.status = ExecutionStatus.RTE; + result.errorOutput = errorOutput; + result.exitCode = exitCode; + result.executionTimeMs = executionTime; + return result; + } + + public static ExecutionResult timeLimitExceeded(Long timeLimit) { + ExecutionResult result = new ExecutionResult(); + result.status = ExecutionStatus.TLE; + result.executionTimeMs = timeLimit; + return result; + } + + public static ExecutionResult memoryLimitExceeded(Long memoryUsed) { + ExecutionResult result = new ExecutionResult(); + result.status = ExecutionStatus.MLE; + result.memoryUsedKb = memoryUsed; + return result; + } + + public static ExecutionResult success(String output, Long executionTime, Long memoryUsed) { + ExecutionResult result = new ExecutionResult(); + result.status = ExecutionStatus.AC; // TODO: Will be verified later with test cases + result.output = output; + result.executionTimeMs = executionTime; + result.memoryUsedKb = memoryUsed; + result.exitCode = 0; + return result; + } + + // Getters and setters + public ExecutionStatus getStatus() { + return status; + } + + public void setStatus(ExecutionStatus status) { + this.status = status; + } + + public String getOutput() { + return output; + } + + public void setOutput(String output) { + this.output = output; + } + + public String getErrorOutput() { + return errorOutput; + } + + public void setErrorOutput(String errorOutput) { + this.errorOutput = errorOutput; + } + + public Long getExecutionTimeMs() { + return executionTimeMs; + } + + public void setExecutionTimeMs(Long executionTimeMs) { + this.executionTimeMs = executionTimeMs; + } + + public Long getMemoryUsedKb() { + return memoryUsedKb; + } + + public void setMemoryUsedKb(Long memoryUsedKb) { + this.memoryUsedKb = memoryUsedKb; + } + + public Integer getExitCode() { + return exitCode; + } + + public void setExitCode(Integer exitCode) { + this.exitCode = exitCode; + } + + public String getCompilationError() { + return compilationError; + } + + public void setCompilationError(String compilationError) { + this.compilationError = compilationError; + } } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/LanguageExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/LanguageExecutor.java index b439d3c..88fa465 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/LanguageExecutor.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/LanguageExecutor.java @@ -1,5 +1,15 @@ package com.github.codehive.worker.sandbox; +import java.io.InputStream; + public interface LanguageExecutor { - + /** + * Execute code with given constraints + * @param sourceCode The source code input stream from MinIO + * @param testInput The test input stream from MinIO (can be null) + * @param timeLimitMs Time limit in milliseconds + * @param memoryLimitMb Memory limit in megabytes + * @return ExecutionResult with verdict and execution details + */ + ExecutionResult execute(InputStream sourceCode, InputStream testInput, Long timeLimitMs, Long memoryLimitMb) throws Exception; } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/c/CExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/c/CExecutor.java index f8533b2..67ecb7a 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/c/CExecutor.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/c/CExecutor.java @@ -1,11 +1,247 @@ package com.github.codehive.worker.sandbox.c; -import org.springframework.stereotype.Component; - +import com.github.codehive.worker.sandbox.ExecutionResult; import com.github.codehive.worker.sandbox.LanguageExecutor; +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.CreateContainerResponse; +import com.github.dockerjava.api.command.WaitContainerResultCallback; +import com.github.dockerjava.api.model.Bind; +import com.github.dockerjava.api.model.HostConfig; +import com.github.dockerjava.api.model.Volume; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.concurrent.*; -@Component("c") +@Component("C") public class CExecutor implements LanguageExecutor { - + private static final Logger logger = LoggerFactory.getLogger(CExecutor.class); + private final DockerClient dockerClient; + private static final String GCC_IMAGE = "gcc:latest"; + private static final long DEFAULT_TIME_LIMIT_MS = 5000L; + private static final long DEFAULT_MEMORY_LIMIT_MB = 256L; + + public CExecutor(DockerClient dockerClient) { + this.dockerClient = dockerClient; + ensureImageExists(); + } + + private void ensureImageExists() { + try { + dockerClient.inspectImageCmd(GCC_IMAGE).exec(); + logger.info("Docker image {} is available", GCC_IMAGE); + } catch (Exception e) { + logger.info("Pulling Docker image {}...", GCC_IMAGE); + try { + dockerClient.pullImageCmd(GCC_IMAGE).start().awaitCompletion(); + logger.info("Successfully pulled image {}", GCC_IMAGE); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + logger.error("Image pull interrupted", ex); + } + } + } + + @Override + public ExecutionResult execute(InputStream sourceCode, InputStream testInput, Long timeLimitMs, Long memoryLimitMb) throws Exception { + timeLimitMs = timeLimitMs != null ? timeLimitMs : DEFAULT_TIME_LIMIT_MS; + memoryLimitMb = memoryLimitMb != null ? memoryLimitMb : DEFAULT_MEMORY_LIMIT_MB; + + Path tempDir = Files.createTempDirectory("c-exec-"); + Path sourceFile = tempDir.resolve("main.c"); + Path inputFile = tempDir.resolve("input.txt"); + + try { + // Save source code + Files.copy(sourceCode, sourceFile, StandardCopyOption.REPLACE_EXISTING); + + // Save test input if provided + if (testInput != null) { + Files.copy(testInput, inputFile, StandardCopyOption.REPLACE_EXISTING); + } + + // Compile + ExecutionResult compileResult = compile(tempDir); + if (compileResult != null) { + return compileResult; // Compilation error + } + + // Execute + return executeCode(tempDir, inputFile, timeLimitMs, memoryLimitMb); + + } finally { + // Cleanup temp directory + deleteDirectory(tempDir); + } + } + + private ExecutionResult compile(Path workDir) { + String containerId = null; + try { + // Create container for compilation + HostConfig hostConfig = HostConfig.newHostConfig() + .withBinds(new Bind(workDir.toString(), new Volume("/workspace"))) + .withMemory(512 * 1024 * 1024L) // 512MB for compilation + .withNetworkMode("none"); + + CreateContainerResponse container = dockerClient.createContainerCmd(GCC_IMAGE) + .withHostConfig(hostConfig) + .withWorkingDir("/workspace") + .withCmd("gcc", "-o", "program", "main.c", "-lm") // -lm for math library + .exec(); + + containerId = container.getId(); + dockerClient.startContainerCmd(containerId).exec(); + + // Wait for compilation + int exitCode = dockerClient.waitContainerCmd(containerId) + .exec(new WaitContainerResultCallback()) + .awaitStatusCode(30, TimeUnit.SECONDS); + + if (exitCode != 0) { + String stderr = getContainerLogs(containerId, true); + return ExecutionResult.compilationError(stderr); + } + + return null; // Success + } catch (Exception e) { + logger.error("Compilation error", e); + return ExecutionResult.compilationError("Compilation failed: " + e.getMessage()); + } finally { + if (containerId != null) { + try { + dockerClient.removeContainerCmd(containerId).withForce(true).exec(); + } catch (Exception e) { + logger.warn("Failed to remove compile container", e); + } + } + } + } + + private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimitMs, long memoryLimitMb) { + String containerId = null; + long startTime = System.currentTimeMillis(); + + try { + HostConfig hostConfig = HostConfig.newHostConfig() + .withBinds(new Bind(workDir.toString(), new Volume("/workspace"))) + .withMemory(memoryLimitMb * 1024 * 1024L) + .withMemorySwap(memoryLimitMb * 1024 * 1024L) // Disable swap + .withCpuQuota(100000L) // 1 CPU + .withNetworkMode("none"); + + String[] cmd; + if (Files.exists(inputFile) && Files.size(inputFile) > 0) { + cmd = new String[]{"sh", "-c", "./program < input.txt"}; + } else { + cmd = new String[]{"./program"}; + } + + CreateContainerResponse container = dockerClient.createContainerCmd(GCC_IMAGE) + .withHostConfig(hostConfig) + .withWorkingDir("/workspace") + .withCmd(cmd) + .exec(); + + containerId = container.getId(); + final String finalContainerId = containerId; + dockerClient.startContainerCmd(containerId).exec(); + + // Wait with timeout + ExecutorService executor = Executors.newSingleThreadExecutor(); + Future future = executor.submit(() -> + dockerClient.waitContainerCmd(finalContainerId) + .exec(new WaitContainerResultCallback()) + .awaitStatusCode() + ); + + Integer exitCode; + try { + exitCode = future.get(timeLimitMs + 1000, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + future.cancel(true); + executor.shutdownNow(); + return ExecutionResult.timeLimitExceeded(timeLimitMs); + } finally { + executor.shutdown(); + } + + long executionTime = System.currentTimeMillis() - startTime; + + // Check for memory limit (Docker kills container if exceeded) + if (exitCode == 137) { // SIGKILL - often means OOM + return ExecutionResult.memoryLimitExceeded(memoryLimitMb * 1024L); + } + + String stdout = getContainerLogs(containerId, false); + String stderr = getContainerLogs(containerId, true); + + if (exitCode != 0) { + return ExecutionResult.runtimeError(stderr, exitCode, executionTime); + } + + // TODO: Compare output with expected output for AC/WA verdict + return ExecutionResult.success(stdout, executionTime, 0L); + + } catch (Exception e) { + logger.error("Execution error", e); + return ExecutionResult.runtimeError(e.getMessage(), -1, System.currentTimeMillis() - startTime); + } finally { + if (containerId != null) { + try { + dockerClient.removeContainerCmd(containerId).withForce(true).exec(); + } catch (Exception e) { + logger.warn("Failed to remove execution container", e); + } + } + } + } + + private String getContainerLogs(String containerId, boolean stderr) { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + dockerClient.logContainerCmd(containerId) + .withStdOut(!stderr) + .withStdErr(stderr) + .exec(new com.github.dockerjava.api.async.ResultCallback.Adapter() { + @Override + public void onNext(com.github.dockerjava.api.model.Frame frame) { + try { + outputStream.write(frame.getPayload()); + } catch (Exception e) { + logger.error("Error reading frame", e); + } + } + }) + .awaitCompletion(5, TimeUnit.SECONDS); + + return outputStream.toString(StandardCharsets.UTF_8); + } catch (Exception e) { + logger.error("Failed to get container logs", e); + return ""; + } + } + + private void deleteDirectory(Path directory) { + try { + Files.walk(directory) + .sorted((a, b) -> -a.compareTo(b)) + .forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (Exception e) { + logger.warn("Failed to delete: " + path, e); + } + }); + } catch (Exception e) { + logger.warn("Failed to delete directory: " + directory, e); + } + } } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/cpp/CPPExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/cpp/CPPExecutor.java index b26fe45..fd80452 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/cpp/CPPExecutor.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/cpp/CPPExecutor.java @@ -1,10 +1,247 @@ package com.github.codehive.worker.sandbox.cpp; +import com.github.codehive.worker.sandbox.ExecutionResult; +import com.github.codehive.worker.sandbox.LanguageExecutor; +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.CreateContainerResponse; +import com.github.dockerjava.api.command.WaitContainerResultCallback; +import com.github.dockerjava.api.model.Bind; +import com.github.dockerjava.api.model.HostConfig; +import com.github.dockerjava.api.model.Volume; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; -import com.github.codehive.worker.sandbox.LanguageExecutor; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.concurrent.*; -@Component("cpp") +@Component("CPP") public class CPPExecutor implements LanguageExecutor { - + private static final Logger logger = LoggerFactory.getLogger(CPPExecutor.class); + private final DockerClient dockerClient; + private static final String GCC_IMAGE = "gcc:latest"; + private static final long DEFAULT_TIME_LIMIT_MS = 5000L; + private static final long DEFAULT_MEMORY_LIMIT_MB = 256L; + + public CPPExecutor(DockerClient dockerClient) { + this.dockerClient = dockerClient; + ensureImageExists(); + } + + private void ensureImageExists() { + try { + dockerClient.inspectImageCmd(GCC_IMAGE).exec(); + logger.info("Docker image {} is available", GCC_IMAGE); + } catch (Exception e) { + logger.info("Pulling Docker image {}...", GCC_IMAGE); + try { + dockerClient.pullImageCmd(GCC_IMAGE).start().awaitCompletion(); + logger.info("Successfully pulled image {}", GCC_IMAGE); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + logger.error("Image pull interrupted", ex); + } + } + } + + @Override + public ExecutionResult execute(InputStream sourceCode, InputStream testInput, Long timeLimitMs, Long memoryLimitMb) throws Exception { + timeLimitMs = timeLimitMs != null ? timeLimitMs : DEFAULT_TIME_LIMIT_MS; + memoryLimitMb = memoryLimitMb != null ? memoryLimitMb : DEFAULT_MEMORY_LIMIT_MB; + + Path tempDir = Files.createTempDirectory("cpp-exec-"); + Path sourceFile = tempDir.resolve("main.cpp"); + Path inputFile = tempDir.resolve("input.txt"); + + try { + // Save source code + Files.copy(sourceCode, sourceFile, StandardCopyOption.REPLACE_EXISTING); + + // Save test input if provided + if (testInput != null) { + Files.copy(testInput, inputFile, StandardCopyOption.REPLACE_EXISTING); + } + + // Compile + ExecutionResult compileResult = compile(tempDir); + if (compileResult != null) { + return compileResult; // Compilation error + } + + // Execute + return executeCode(tempDir, inputFile, timeLimitMs, memoryLimitMb); + + } finally { + // Cleanup temp directory + deleteDirectory(tempDir); + } + } + + private ExecutionResult compile(Path workDir) { + String containerId = null; + try { + // Create container for compilation + HostConfig hostConfig = HostConfig.newHostConfig() + .withBinds(new Bind(workDir.toString(), new Volume("/workspace"))) + .withMemory(512 * 1024 * 1024L) // 512MB for compilation + .withNetworkMode("none"); + + CreateContainerResponse container = dockerClient.createContainerCmd(GCC_IMAGE) + .withHostConfig(hostConfig) + .withWorkingDir("/workspace") + .withCmd("g++", "-o", "program", "main.cpp", "-std=c++17", "-lm") // C++17 standard + .exec(); + + containerId = container.getId(); + dockerClient.startContainerCmd(containerId).exec(); + + // Wait for compilation + int exitCode = dockerClient.waitContainerCmd(containerId) + .exec(new WaitContainerResultCallback()) + .awaitStatusCode(30, TimeUnit.SECONDS); + + if (exitCode != 0) { + String stderr = getContainerLogs(containerId, true); + return ExecutionResult.compilationError(stderr); + } + + return null; // Success + } catch (Exception e) { + logger.error("Compilation error", e); + return ExecutionResult.compilationError("Compilation failed: " + e.getMessage()); + } finally { + if (containerId != null) { + try { + dockerClient.removeContainerCmd(containerId).withForce(true).exec(); + } catch (Exception e) { + logger.warn("Failed to remove compile container", e); + } + } + } + } + + private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimitMs, long memoryLimitMb) { + String containerId = null; + long startTime = System.currentTimeMillis(); + + try { + HostConfig hostConfig = HostConfig.newHostConfig() + .withBinds(new Bind(workDir.toString(), new Volume("/workspace"))) + .withMemory(memoryLimitMb * 1024 * 1024L) + .withMemorySwap(memoryLimitMb * 1024 * 1024L) // Disable swap + .withCpuQuota(100000L) // 1 CPU + .withNetworkMode("none"); + + String[] cmd; + if (Files.exists(inputFile) && Files.size(inputFile) > 0) { + cmd = new String[]{"sh", "-c", "./program < input.txt"}; + } else { + cmd = new String[]{"./program"}; + } + + CreateContainerResponse container = dockerClient.createContainerCmd(GCC_IMAGE) + .withHostConfig(hostConfig) + .withWorkingDir("/workspace") + .withCmd(cmd) + .exec(); + + containerId = container.getId(); + final String finalContainerId = containerId; + dockerClient.startContainerCmd(containerId).exec(); + + // Wait with timeout + ExecutorService executor = Executors.newSingleThreadExecutor(); + Future future = executor.submit(() -> + dockerClient.waitContainerCmd(finalContainerId) + .exec(new WaitContainerResultCallback()) + .awaitStatusCode() + ); + + Integer exitCode; + try { + exitCode = future.get(timeLimitMs + 1000, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + future.cancel(true); + executor.shutdownNow(); + return ExecutionResult.timeLimitExceeded(timeLimitMs); + } finally { + executor.shutdown(); + } + + long executionTime = System.currentTimeMillis() - startTime; + + // Check for memory limit (Docker kills container if exceeded) + if (exitCode == 137) { // SIGKILL - often means OOM + return ExecutionResult.memoryLimitExceeded(memoryLimitMb * 1024L); + } + + String stdout = getContainerLogs(containerId, false); + String stderr = getContainerLogs(containerId, true); + + if (exitCode != 0) { + return ExecutionResult.runtimeError(stderr, exitCode, executionTime); + } + + // TODO: Compare output with expected output for AC/WA verdict + return ExecutionResult.success(stdout, executionTime, 0L); + + } catch (Exception e) { + logger.error("Execution error", e); + return ExecutionResult.runtimeError(e.getMessage(), -1, System.currentTimeMillis() - startTime); + } finally { + if (containerId != null) { + try { + dockerClient.removeContainerCmd(containerId).withForce(true).exec(); + } catch (Exception e) { + logger.warn("Failed to remove execution container", e); + } + } + } + } + + private String getContainerLogs(String containerId, boolean stderr) { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + dockerClient.logContainerCmd(containerId) + .withStdOut(!stderr) + .withStdErr(stderr) + .exec(new com.github.dockerjava.api.async.ResultCallback.Adapter() { + @Override + public void onNext(com.github.dockerjava.api.model.Frame frame) { + try { + outputStream.write(frame.getPayload()); + } catch (Exception e) { + logger.error("Error reading frame", e); + } + } + }) + .awaitCompletion(5, TimeUnit.SECONDS); + + return outputStream.toString(StandardCharsets.UTF_8); + } catch (Exception e) { + logger.error("Failed to get container logs", e); + return ""; + } + } + + private void deleteDirectory(Path directory) { + try { + Files.walk(directory) + .sorted((a, b) -> -a.compareTo(b)) + .forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (Exception e) { + logger.warn("Failed to delete: " + path, e); + } + }); + } catch (Exception e) { + logger.warn("Failed to delete directory: " + directory, e); + } + } } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/factory/LanguageExecutorFactory.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/factory/LanguageExecutorFactory.java index 9d603a3..6049e36 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/factory/LanguageExecutorFactory.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/factory/LanguageExecutorFactory.java @@ -2,19 +2,33 @@ import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; +import com.github.codehive.worker.model.enums.Language; import com.github.codehive.worker.sandbox.LanguageExecutor; @Component public class LanguageExecutorFactory { + private static final Logger logger = LoggerFactory.getLogger(LanguageExecutorFactory.class); private final Map executors; public LanguageExecutorFactory(Map executors) { this.executors = executors; + logger.info("LanguageExecutorFactory initialized with executors: {}", executors.keySet()); } - public LanguageExecutor getExecutor(String language) { - return executors.get(language); + public LanguageExecutor getExecutor(Language language) { + if (language == null) { + throw new IllegalArgumentException("Language cannot be null"); + } + + LanguageExecutor executor = executors.get(language.name()); + if (executor == null) { + throw new IllegalArgumentException("No executor found for language: " + language); + } + + return executor; } } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java index 6d0c8ba..fe488ea 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java @@ -1,10 +1,247 @@ package com.github.codehive.worker.sandbox.java; +import com.github.codehive.worker.sandbox.ExecutionResult; +import com.github.codehive.worker.sandbox.LanguageExecutor; +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.CreateContainerResponse; +import com.github.dockerjava.api.command.WaitContainerResultCallback; +import com.github.dockerjava.api.model.Bind; +import com.github.dockerjava.api.model.HostConfig; +import com.github.dockerjava.api.model.Volume; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; -import com.github.codehive.worker.sandbox.LanguageExecutor; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.concurrent.*; -@Component("java") +@Component("JAVA") public class JavaExecutor implements LanguageExecutor { - + private static final Logger logger = LoggerFactory.getLogger(JavaExecutor.class); + private final DockerClient dockerClient; + private static final String JAVA_IMAGE = "openjdk:21-slim"; + private static final long DEFAULT_TIME_LIMIT_MS = 5000L; + private static final long DEFAULT_MEMORY_LIMIT_MB = 256L; + + public JavaExecutor(DockerClient dockerClient) { + this.dockerClient = dockerClient; + ensureImageExists(); + } + + private void ensureImageExists() { + try { + dockerClient.inspectImageCmd(JAVA_IMAGE).exec(); + logger.info("Docker image {} is available", JAVA_IMAGE); + } catch (Exception e) { + logger.info("Pulling Docker image {}...", JAVA_IMAGE); + try { + dockerClient.pullImageCmd(JAVA_IMAGE).start().awaitCompletion(); + logger.info("Successfully pulled image {}", JAVA_IMAGE); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + logger.error("Image pull interrupted", ex); + } + } + } + + @Override + public ExecutionResult execute(InputStream sourceCode, InputStream testInput, Long timeLimitMs, Long memoryLimitMb) throws Exception { + timeLimitMs = timeLimitMs != null ? timeLimitMs : DEFAULT_TIME_LIMIT_MS; + memoryLimitMb = memoryLimitMb != null ? memoryLimitMb : DEFAULT_MEMORY_LIMIT_MB; + + Path tempDir = Files.createTempDirectory("java-exec-"); + Path sourceFile = tempDir.resolve("Main.java"); + Path inputFile = tempDir.resolve("input.txt"); + + try { + // Save source code + Files.copy(sourceCode, sourceFile, StandardCopyOption.REPLACE_EXISTING); + + // Save test input if provided + if (testInput != null) { + Files.copy(testInput, inputFile, StandardCopyOption.REPLACE_EXISTING); + } + + // Compile + ExecutionResult compileResult = compile(tempDir); + if (compileResult != null) { + return compileResult; // Compilation error + } + + // Execute + return executeCode(tempDir, inputFile, timeLimitMs, memoryLimitMb); + + } finally { + // Cleanup temp directory + deleteDirectory(tempDir); + } + } + + private ExecutionResult compile(Path workDir) { + String containerId = null; + try { + // Create container for compilation + HostConfig hostConfig = HostConfig.newHostConfig() + .withBinds(new Bind(workDir.toString(), new Volume("/workspace"))) + .withMemory(512 * 1024 * 1024L) // 512MB for compilation + .withNetworkMode("none"); + + CreateContainerResponse container = dockerClient.createContainerCmd(JAVA_IMAGE) + .withHostConfig(hostConfig) + .withWorkingDir("/workspace") + .withCmd("javac", "Main.java") + .exec(); + + containerId = container.getId(); + dockerClient.startContainerCmd(containerId).exec(); + + // Wait for compilation + int exitCode = dockerClient.waitContainerCmd(containerId) + .exec(new WaitContainerResultCallback()) + .awaitStatusCode(30, TimeUnit.SECONDS); + + if (exitCode != 0) { + String stderr = getContainerLogs(containerId, true); + return ExecutionResult.compilationError(stderr); + } + + return null; // Success + } catch (Exception e) { + logger.error("Compilation error", e); + return ExecutionResult.compilationError("Compilation failed: " + e.getMessage()); + } finally { + if (containerId != null) { + try { + dockerClient.removeContainerCmd(containerId).withForce(true).exec(); + } catch (Exception e) { + logger.warn("Failed to remove compile container", e); + } + } + } + } + + private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimitMs, long memoryLimitMb) { + String containerId = null; + long startTime = System.currentTimeMillis(); + + try { + HostConfig hostConfig = HostConfig.newHostConfig() + .withBinds(new Bind(workDir.toString(), new Volume("/workspace"))) + .withMemory(memoryLimitMb * 1024 * 1024L) + .withMemorySwap(memoryLimitMb * 1024 * 1024L) // Disable swap + .withCpuQuota(100000L) // 1 CPU + .withNetworkMode("none"); + + String[] cmd; + if (Files.exists(inputFile) && Files.size(inputFile) > 0) { + cmd = new String[]{"sh", "-c", "java Main < input.txt"}; + } else { + cmd = new String[]{"java", "Main"}; + } + + CreateContainerResponse container = dockerClient.createContainerCmd(JAVA_IMAGE) + .withHostConfig(hostConfig) + .withWorkingDir("/workspace") + .withCmd(cmd) + .exec(); + + containerId = container.getId(); + final String finalContainerId = containerId; + dockerClient.startContainerCmd(containerId).exec(); + + // Wait with timeout + ExecutorService executor = Executors.newSingleThreadExecutor(); + Future future = executor.submit(() -> + dockerClient.waitContainerCmd(finalContainerId) + .exec(new WaitContainerResultCallback()) + .awaitStatusCode() + ); + + Integer exitCode; + try { + exitCode = future.get(timeLimitMs + 1000, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + future.cancel(true); + executor.shutdownNow(); + return ExecutionResult.timeLimitExceeded(timeLimitMs); + } finally { + executor.shutdown(); + } + + long executionTime = System.currentTimeMillis() - startTime; + + // Check for memory limit (best effort - Docker kills container if exceeded) + if (exitCode == 137) { // SIGKILL - often means OOM + return ExecutionResult.memoryLimitExceeded(memoryLimitMb * 1024L); + } + + String stdout = getContainerLogs(containerId, false); + String stderr = getContainerLogs(containerId, true); + + if (exitCode != 0) { + return ExecutionResult.runtimeError(stderr, exitCode, executionTime); + } + + // TODO: Compare output with expected output for AC/WA verdict + return ExecutionResult.success(stdout, executionTime, 0L); + + } catch (Exception e) { + logger.error("Execution error", e); + return ExecutionResult.runtimeError(e.getMessage(), -1, System.currentTimeMillis() - startTime); + } finally { + if (containerId != null) { + try { + dockerClient.removeContainerCmd(containerId).withForce(true).exec(); + } catch (Exception e) { + logger.warn("Failed to remove execution container", e); + } + } + } + } + + private String getContainerLogs(String containerId, boolean stderr) { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + dockerClient.logContainerCmd(containerId) + .withStdOut(!stderr) + .withStdErr(stderr) + .exec(new com.github.dockerjava.api.async.ResultCallback.Adapter() { + @Override + public void onNext(com.github.dockerjava.api.model.Frame frame) { + try { + outputStream.write(frame.getPayload()); + } catch (Exception e) { + logger.error("Error reading frame", e); + } + } + }) + .awaitCompletion(5, TimeUnit.SECONDS); + + return outputStream.toString(StandardCharsets.UTF_8); + } catch (Exception e) { + logger.error("Failed to get container logs", e); + return ""; + } + } + + private void deleteDirectory(Path directory) { + try { + Files.walk(directory) + .sorted((a, b) -> -a.compareTo(b)) + .forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (Exception e) { + logger.warn("Failed to delete: " + path, e); + } + }); + } catch (Exception e) { + logger.warn("Failed to delete directory: " + directory, e); + } + } } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/python/PythonExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/python/PythonExecutor.java index 6ed9682..f7a2923 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/python/PythonExecutor.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/python/PythonExecutor.java @@ -1,10 +1,203 @@ package com.github.codehive.worker.sandbox.python; +import com.github.codehive.worker.sandbox.ExecutionResult; +import com.github.codehive.worker.sandbox.LanguageExecutor; +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.CreateContainerResponse; +import com.github.dockerjava.api.command.WaitContainerResultCallback; +import com.github.dockerjava.api.model.Bind; +import com.github.dockerjava.api.model.HostConfig; +import com.github.dockerjava.api.model.Volume; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; -import com.github.codehive.worker.sandbox.LanguageExecutor; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.concurrent.*; -@Component("python") +@Component("PYTHON") public class PythonExecutor implements LanguageExecutor { - + private static final Logger logger = LoggerFactory.getLogger(PythonExecutor.class); + private final DockerClient dockerClient; + private static final String PYTHON_IMAGE = "python:3.11-slim"; + private static final long DEFAULT_TIME_LIMIT_MS = 5000L; + private static final long DEFAULT_MEMORY_LIMIT_MB = 256L; + + public PythonExecutor(DockerClient dockerClient) { + this.dockerClient = dockerClient; + ensureImageExists(); + } + + private void ensureImageExists() { + try { + dockerClient.inspectImageCmd(PYTHON_IMAGE).exec(); + logger.info("Docker image {} is available", PYTHON_IMAGE); + } catch (Exception e) { + logger.info("Pulling Docker image {}...", PYTHON_IMAGE); + try { + dockerClient.pullImageCmd(PYTHON_IMAGE).start().awaitCompletion(); + logger.info("Successfully pulled image {}", PYTHON_IMAGE); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + logger.error("Image pull interrupted", ex); + } + } + } + + @Override + public ExecutionResult execute(InputStream sourceCode, InputStream testInput, Long timeLimitMs, Long memoryLimitMb) throws Exception { + timeLimitMs = timeLimitMs != null ? timeLimitMs : DEFAULT_TIME_LIMIT_MS; + memoryLimitMb = memoryLimitMb != null ? memoryLimitMb : DEFAULT_MEMORY_LIMIT_MB; + + Path tempDir = Files.createTempDirectory("python-exec-"); + Path sourceFile = tempDir.resolve("main.py"); + Path inputFile = tempDir.resolve("input.txt"); + + try { + // Save source code + Files.copy(sourceCode, sourceFile, StandardCopyOption.REPLACE_EXISTING); + + // Save test input if provided + if (testInput != null) { + Files.copy(testInput, inputFile, StandardCopyOption.REPLACE_EXISTING); + } + + // Execute (Python doesn't need compilation) + return executeCode(tempDir, inputFile, timeLimitMs, memoryLimitMb); + + } finally { + // Cleanup temp directory + deleteDirectory(tempDir); + } + } + + private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimitMs, long memoryLimitMb) { + String containerId = null; + long startTime = System.currentTimeMillis(); + + try { + HostConfig hostConfig = HostConfig.newHostConfig() + .withBinds(new Bind(workDir.toString(), new Volume("/workspace"))) + .withMemory(memoryLimitMb * 1024 * 1024L) + .withMemorySwap(memoryLimitMb * 1024 * 1024L) // Disable swap + .withCpuQuota(100000L) // 1 CPU + .withNetworkMode("none"); + + String[] cmd; + if (Files.exists(inputFile) && Files.size(inputFile) > 0) { + cmd = new String[]{"sh", "-c", "python main.py < input.txt"}; + } else { + cmd = new String[]{"python", "main.py"}; + } + + CreateContainerResponse container = dockerClient.createContainerCmd(PYTHON_IMAGE) + .withHostConfig(hostConfig) + .withWorkingDir("/workspace") + .withCmd(cmd) + .exec(); + + containerId = container.getId(); + final String finalContainerId = containerId; + dockerClient.startContainerCmd(containerId).exec(); + + // Wait with timeout + ExecutorService executor = Executors.newSingleThreadExecutor(); + Future future = executor.submit(() -> + dockerClient.waitContainerCmd(finalContainerId) + .exec(new WaitContainerResultCallback()) + .awaitStatusCode() + ); + + Integer exitCode; + try { + exitCode = future.get(timeLimitMs + 1000, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + future.cancel(true); + executor.shutdownNow(); + return ExecutionResult.timeLimitExceeded(timeLimitMs); + } finally { + executor.shutdown(); + } + + long executionTime = System.currentTimeMillis() - startTime; + + // Check for memory limit (Docker kills container if exceeded) + if (exitCode == 137) { // SIGKILL - often means OOM + return ExecutionResult.memoryLimitExceeded(memoryLimitMb * 1024L); + } + + String stdout = getContainerLogs(containerId, false); + String stderr = getContainerLogs(containerId, true); + + // Python syntax errors will exit with non-zero code + if (exitCode != 0 && stderr.contains("SyntaxError")) { + return ExecutionResult.compilationError(stderr); + } + + if (exitCode != 0) { + return ExecutionResult.runtimeError(stderr, exitCode, executionTime); + } + + // TODO: Compare output with expected output for AC/WA verdict + return ExecutionResult.success(stdout, executionTime, 0L); + + } catch (Exception e) { + logger.error("Execution error", e); + return ExecutionResult.runtimeError(e.getMessage(), -1, System.currentTimeMillis() - startTime); + } finally { + if (containerId != null) { + try { + dockerClient.removeContainerCmd(containerId).withForce(true).exec(); + } catch (Exception e) { + logger.warn("Failed to remove execution container", e); + } + } + } + } + + private String getContainerLogs(String containerId, boolean stderr) { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + dockerClient.logContainerCmd(containerId) + .withStdOut(!stderr) + .withStdErr(stderr) + .exec(new com.github.dockerjava.api.async.ResultCallback.Adapter() { + @Override + public void onNext(com.github.dockerjava.api.model.Frame frame) { + try { + outputStream.write(frame.getPayload()); + } catch (Exception e) { + logger.error("Error reading frame", e); + } + } + }) + .awaitCompletion(5, TimeUnit.SECONDS); + + return outputStream.toString(StandardCharsets.UTF_8); + } catch (Exception e) { + logger.error("Failed to get container logs", e); + return ""; + } + } + + private void deleteDirectory(Path directory) { + try { + Files.walk(directory) + .sorted((a, b) -> -a.compareTo(b)) + .forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (Exception e) { + logger.warn("Failed to delete: " + path, e); + } + }); + } catch (Exception e) { + logger.warn("Failed to delete directory: " + directory, e); + } + } } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/service/ExecutionService.java b/codehive-worker/src/main/java/com/github/codehive/worker/service/ExecutionService.java deleted file mode 100644 index f8a1175..0000000 --- a/codehive-worker/src/main/java/com/github/codehive/worker/service/ExecutionService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.github.codehive.worker.service; - -public class ExecutionService { - -} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/service/ObjectStorageService.java b/codehive-worker/src/main/java/com/github/codehive/worker/service/ObjectStorageService.java new file mode 100644 index 0000000..c1fc9bf --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/service/ObjectStorageService.java @@ -0,0 +1,60 @@ +package com.github.codehive.worker.service; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.springframework.stereotype.Service; +import io.minio.GetObjectArgs; +import io.minio.MinioClient; +import io.minio.PutObjectArgs; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Service +public class ObjectStorageService { + private static final Logger logger = LoggerFactory.getLogger(ObjectStorageService.class); + private final MinioClient minioClient; + private final String bucketName = System.getProperty("minio.bucketName", "codehive"); + + public ObjectStorageService(MinioClient minioClient) { + this.minioClient = minioClient; + } + + public InputStream download(String objectKey) throws Exception { + return minioClient.getObject( + GetObjectArgs.builder() + .bucket(bucketName) + .object(objectKey) + .build() + ); + } + + public void upload(String objectKey, String content) throws Exception { + byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8); + ByteArrayInputStream stream = new ByteArrayInputStream(contentBytes); + + minioClient.putObject( + PutObjectArgs.builder() + .bucket(bucketName) + .object(objectKey) + .stream(stream, contentBytes.length, -1) + .contentType("text/plain") + .build() + ); + + logger.info("Uploaded content to MinIO: {}", objectKey); + } + + public void upload(String objectKey, InputStream content, long size) throws Exception { + minioClient.putObject( + PutObjectArgs.builder() + .bucket(bucketName) + .object(objectKey) + .stream(content, size, -1) + .contentType("application/octet-stream") + .build() + ); + + logger.info("Uploaded stream to MinIO: {}", objectKey); + } +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/service/OutputComparatorService.java b/codehive-worker/src/main/java/com/github/codehive/worker/service/OutputComparatorService.java new file mode 100644 index 0000000..5208b86 --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/service/OutputComparatorService.java @@ -0,0 +1,127 @@ +package com.github.codehive.worker.service; + +import com.github.codehive.worker.model.enums.ComparatorType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class OutputComparatorService { + private static final Logger logger = LoggerFactory.getLogger(OutputComparatorService.class); + private static final double EPSILON = 1e-9; + + /** + * Compare two outputs based on the comparator type + * @param expected Expected output + * @param actual Actual output from execution + * @param comparatorType Type of comparison to perform + * @return true if outputs match, false otherwise + */ + public boolean compare(String expected, String actual, ComparatorType comparatorType) { + if (expected == null && actual == null) { + return true; + } + if (expected == null || actual == null) { + return false; + } + + switch (comparatorType) { + case EXACT_MATCH: + return compareExact(expected, actual); + case FLOATING_POINT: + return compareFloatingPoint(expected, actual); + default: + logger.warn("Unknown comparator type: {}, falling back to exact match", comparatorType); + return compareExact(expected, actual); + } + } + + /** + * Exact string comparison (trimmed lines, ignoring trailing whitespace) + */ + private boolean compareExact(String expected, String actual) { + List expectedLines = normalizeLines(expected); + List actualLines = normalizeLines(actual); + + if (expectedLines.size() != actualLines.size()) { + logger.debug("Line count mismatch: expected {} lines, got {} lines", + expectedLines.size(), actualLines.size()); + return false; + } + + for (int i = 0; i < expectedLines.size(); i++) { + if (!expectedLines.get(i).equals(actualLines.get(i))) { + logger.debug("Line {} mismatch: expected '{}', got '{}'", + i + 1, expectedLines.get(i), actualLines.get(i)); + return false; + } + } + + return true; + } + + /** + * Floating point comparison with tolerance + * Compares token by token, treating numbers with epsilon tolerance + */ + private boolean compareFloatingPoint(String expected, String actual) { + List expectedLines = normalizeLines(expected); + List actualLines = normalizeLines(actual); + + if (expectedLines.size() != actualLines.size()) { + logger.debug("Line count mismatch: expected {} lines, got {} lines", + expectedLines.size(), actualLines.size()); + return false; + } + + for (int i = 0; i < expectedLines.size(); i++) { + String[] expectedTokens = expectedLines.get(i).split("\\s+"); + String[] actualTokens = actualLines.get(i).split("\\s+"); + + if (expectedTokens.length != actualTokens.length) { + logger.debug("Token count mismatch on line {}: expected {} tokens, got {} tokens", + i + 1, expectedTokens.length, actualTokens.length); + return false; + } + + for (int j = 0; j < expectedTokens.length; j++) { + if (!compareTokens(expectedTokens[j], actualTokens[j])) { + logger.debug("Token mismatch at line {} position {}: expected '{}', got '{}'", + i + 1, j + 1, expectedTokens[j], actualTokens[j]); + return false; + } + } + } + + return true; + } + + /** + * Compare individual tokens, treating numbers with epsilon tolerance + */ + private boolean compareTokens(String expected, String actual) { + // Try to parse as numbers first + try { + double expectedNum = Double.parseDouble(expected); + double actualNum = Double.parseDouble(actual); + return Math.abs(expectedNum - actualNum) <= EPSILON; + } catch (NumberFormatException e) { + // Not numbers, compare as strings + return expected.equals(actual); + } + } + + /** + * Normalize output into list of trimmed non-empty lines + */ + private List normalizeLines(String output) { + return Arrays.stream(output.split("\\r?\\n")) + .map(String::trim) + .filter(line -> !line.isEmpty()) + .collect(Collectors.toList()); + } +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java b/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java new file mode 100644 index 0000000..9310373 --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java @@ -0,0 +1,272 @@ +package com.github.codehive.worker.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.codehive.worker.model.dto.ExecutionReport; +import com.github.codehive.worker.model.dto.TestCaseResult; +import com.github.codehive.worker.model.dto.queue.ExecutionJob; +import com.github.codehive.worker.model.enums.ComparatorType; +import com.github.codehive.worker.model.enums.ExecutionStatus; +import com.github.codehive.worker.model.enums.ExecutionType; +import com.github.codehive.worker.sandbox.ExecutionResult; +import com.github.codehive.worker.sandbox.LanguageExecutor; +import com.github.codehive.worker.sandbox.factory.LanguageExecutorFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +@Service +public class TestExecutionService { + private static final Logger logger = LoggerFactory.getLogger(TestExecutionService.class); + + private final LanguageExecutorFactory executorFactory; + private final ObjectStorageService objectStorageService; + private final OutputComparatorService outputComparatorService; + private final ObjectMapper objectMapper; + + public TestExecutionService(LanguageExecutorFactory executorFactory, + ObjectStorageService objectStorageService, + OutputComparatorService outputComparatorService) { + this.executorFactory = executorFactory; + this.objectStorageService = objectStorageService; + this.outputComparatorService = outputComparatorService; + this.objectMapper = new ObjectMapper(); + } + + /** + * Execute the submission against all test cases + */ + public ExecutionReport executeJob(ExecutionJob job) { + ExecutionReport report = new ExecutionReport(job.getId()); + + try { + // Download source code + InputStream sourceCode = objectStorageService.download(job.getSource()); + + // Get executor + LanguageExecutor executor = executorFactory.getExecutor(job.getLanguage()); + + // First, test compilation with a simple run (no input) + ExecutionResult compileTest = executor.execute( + objectStorageService.download(job.getSource()), + null, + job.getTimeLimitMs(), + job.getMemoryLimitMb() + ); + + if (compileTest.getStatus() == ExecutionStatus.CE) { + report.setCompilationError(compileTest.getCompilationError()); + report.determineOverallStatus(); + return report; + } + + // Execute based on execution type + if (job.getExecutionType() == ExecutionType.DEFINITIVE) { + executeDefinitiveTests(job, executor, report); + } else { + executePracticeTests(job, executor, report); + } + + report.determineOverallStatus(); + + // Upload report to MinIO + if (job.getOutputPath() != null) { + uploadReport(job.getOutputPath(), report); + } + + } catch (Exception e) { + logger.error("Failed to execute job: id={}", job.getId(), e); + report.setCompilationError("Execution failed: " + e.getMessage()); + report.determineOverallStatus(); + } + + return report; + } + + /** + * Execute DEFINITIVE tests: run against stored test cases, compare with expected outputs + */ + private void executeDefinitiveTests(ExecutionJob job, LanguageExecutor executor, ExecutionReport report) { + logger.info("Executing DEFINITIVE tests: {} test cases", job.getNumTests()); + + for (int i = 1; i <= job.getNumTests(); i++) { + try { + // Construct paths for test input and expected output + String inputPath = job.getTestsPath() + "tc-" + i + "/tc-" + i + ".in"; + String outputPath = job.getTestsPath() + "tc-" + i + "/tc-" + i + ".out"; + + logger.debug("Test case {}: input={}, output={}", i, inputPath, outputPath); + + // Download test input and expected output + InputStream testInput = objectStorageService.download(inputPath); + InputStream expectedOutputStream = objectStorageService.download(outputPath); + String expectedOutput = new String(expectedOutputStream.readAllBytes(), StandardCharsets.UTF_8); + + // Execute with test input + ExecutionResult result = executor.execute( + objectStorageService.download(job.getSource()), + testInput, + job.getTimeLimitMs(), + job.getMemoryLimitMb() + ); + + // Create test case result + TestCaseResult testResult = new TestCaseResult( + i, + result.getStatus(), + result.getOutput(), + expectedOutput, + result.getExecutionTimeMs(), + result.getMemoryUsedKb() + ); + + testResult.setErrorOutput(result.getErrorOutput()); + + // Compare outputs if execution was successful + if (result.getStatus() == ExecutionStatus.AC) { + boolean matches = outputComparatorService.compare( + expectedOutput, + result.getOutput(), + job.getComparatorType() + ); + + if (!matches) { + testResult.setStatus(ExecutionStatus.WA); + testResult.setFeedback("Output does not match expected output"); + } else { + testResult.setFeedback("Test passed"); + } + } else { + testResult.setFeedback(getStatusFeedback(result.getStatus())); + } + + report.addTestCaseResult(testResult); + + } catch (Exception e) { + logger.error("Failed to execute test case {}", i, e); + TestCaseResult errorResult = new TestCaseResult(); + errorResult.setTestCaseNumber(i); + errorResult.setStatus(ExecutionStatus.RTE); + errorResult.setFeedback("Test execution failed: " + e.getMessage()); + report.addTestCaseResult(errorResult); + } + } + } + + /** + * Execute PRACTICE tests: run against inline test cases, compare with reference solution + */ + private void executePracticeTests(ExecutionJob job, LanguageExecutor executor, ExecutionReport report) { + logger.info("Executing PRACTICE tests: {} test cases", job.getTestCases().size()); + + // Get reference executor + LanguageExecutor referenceExecutor = executorFactory.getExecutor(job.getReferenceLanguage()); + + for (int i = 0; i < job.getTestCases().size(); i++) { + try { + String testInput = job.getTestCases().get(i); + InputStream testInputStream = new ByteArrayInputStream(testInput.getBytes(StandardCharsets.UTF_8)); + + // Execute reference solution to get expected output + ExecutionResult referenceResult = referenceExecutor.execute( + objectStorageService.download(job.getReference()), + new ByteArrayInputStream(testInput.getBytes(StandardCharsets.UTF_8)), + job.getTimeLimitMs(), + job.getMemoryLimitMb() + ); + + if (referenceResult.getStatus() != ExecutionStatus.AC) { + logger.error("Reference solution failed on test case {}: {}", i + 1, referenceResult.getStatus()); + TestCaseResult errorResult = new TestCaseResult(); + errorResult.setTestCaseNumber(i + 1); + errorResult.setStatus(ExecutionStatus.RTE); + errorResult.setFeedback("Reference solution failed"); + report.addTestCaseResult(errorResult); + continue; + } + + String expectedOutput = referenceResult.getOutput(); + + // Execute student submission + ExecutionResult result = executor.execute( + objectStorageService.download(job.getSource()), + new ByteArrayInputStream(testInput.getBytes(StandardCharsets.UTF_8)), + job.getTimeLimitMs(), + job.getMemoryLimitMb() + ); + + // Create test case result + TestCaseResult testResult = new TestCaseResult( + i + 1, + result.getStatus(), + result.getOutput(), + expectedOutput, + result.getExecutionTimeMs(), + result.getMemoryUsedKb() + ); + + testResult.setErrorOutput(result.getErrorOutput()); + + // Compare outputs if execution was successful + if (result.getStatus() == ExecutionStatus.AC) { + boolean matches = outputComparatorService.compare( + expectedOutput, + result.getOutput(), + job.getComparatorType() + ); + + if (!matches) { + testResult.setStatus(ExecutionStatus.WA); + testResult.setFeedback("Output does not match expected output"); + } else { + testResult.setFeedback("Test passed"); + } + } else { + testResult.setFeedback(getStatusFeedback(result.getStatus())); + } + + report.addTestCaseResult(testResult); + + } catch (Exception e) { + logger.error("Failed to execute test case {}", i + 1, e); + TestCaseResult errorResult = new TestCaseResult(); + errorResult.setTestCaseNumber(i + 1); + errorResult.setStatus(ExecutionStatus.RTE); + errorResult.setFeedback("Test execution failed: " + e.getMessage()); + report.addTestCaseResult(errorResult); + } + } + } + + private String getStatusFeedback(ExecutionStatus status) { + switch (status) { + case TLE: + return "Time limit exceeded"; + case MLE: + return "Memory limit exceeded"; + case RTE: + return "Runtime error"; + case CE: + return "Compilation error"; + case WA: + return "Wrong answer"; + case AC: + return "Accepted"; + default: + return "Unknown status"; + } + } + + private void uploadReport(String outputPath, ExecutionReport report) { + try { + String jsonReport = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(report); + objectStorageService.upload(outputPath, jsonReport); + logger.info("Uploaded execution report to: {}", outputPath); + } catch (Exception e) { + logger.error("Failed to upload execution report", e); + } + } +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/util/ObjectKeyBuilder.java b/codehive-worker/src/main/java/com/github/codehive/worker/util/ObjectKeyBuilder.java new file mode 100644 index 0000000..246bc3f --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/util/ObjectKeyBuilder.java @@ -0,0 +1,31 @@ +package com.github.codehive.worker.util; + +public class ObjectKeyBuilder { + public static String testCaseInput(Long assignmentId, Long testCaseId) { + return String.format("test-suites/assignments/%d/tc-%d/tc%d.in", assignmentId, testCaseId, testCaseId); + } + + public static String testCaseOutput(Long assignmentId, Long testCaseId) { + return String.format("test-suites/assignments/%d/tc-%d/tc%d.out", assignmentId, testCaseId, testCaseId); + } + + public static String submissionSourceCode(Long assignmentId, Long submissionId, String fileExtension, Long groupId) { + return String.format("submissions/groups/%d/assignments/%d/submission-%d/Main.%s", groupId, assignmentId, submissionId, fileExtension); + } + + public static String executionOutput(Long submissionId, Long executionId, Long groupId, String fileExtension) { + return String.format("executions/groups/%d/assignments/%d/execution-%d/output.%s", groupId, submissionId, executionId, fileExtension); + } + + public static String referenceSolutionSourceCode(Long assignmentId, Long referenceSolutionId, String fileExtension) { + return String.format("reference-solutions/assignments/%d/reference-solution-%d/Main.%s", assignmentId, referenceSolutionId, fileExtension); + } + + public static String executionTestCaseOutput(Long executionId, String fileExtension) { + return String.format("test-execution/execution-%d/output.%s", executionId, fileExtension); + } + + public static String executionTestSource(Long executionId, String fileExtension) { + return String.format("test-execution/execution-%d/Main.%s", executionId, fileExtension); + } +} diff --git a/codehive-worker/src/main/resources/application.properties b/codehive-worker/src/main/resources/application.properties index 92fef53..17744cd 100644 --- a/codehive-worker/src/main/resources/application.properties +++ b/codehive-worker/src/main/resources/application.properties @@ -1 +1,13 @@ spring.application.name=worker +# Minio Configuration +minio.url=${MINIO_URL} +minio.accessKey=${MINIO_ACCESS_KEY} +minio.secretKey=${MINIO_SECRET_KEY} +minio.bucketName=${MINIO_BUCKET_NAME} + +# RabbitMQ Configuration +spring.rabbitmq.host=${RABBITMQ_HOST} +spring.rabbitmq.port=${RABBITMQ_PORT} +spring.rabbitmq.username=${RABBITMQ_USERNAME} +spring.rabbitmq.password=${RABBITMQ_PASSWORD} +spring.rabbitmq.queue=${RABBITMQ_QUEUE} \ No newline at end of file From 22a81657d06e00b3e9cf1f66c1a360cc6b020966 Mon Sep 17 00:00:00 2001 From: IrminDev Date: Thu, 22 Jan 2026 19:38:07 -0600 Subject: [PATCH 06/32] Added jakarta persistance, DTOs, mappers and repositories for code execution --- .../codehive/model/dto/AssignmentDTO.java | 119 +++++++++++++ .../codehive/model/dto/ExecutionDTO.java | 83 +++++++++ .../model/dto/ReferenceSolutionDTO.java | 35 ++++ .../codehive/model/dto/SubmissionDTO.java | 46 +++++ .../codehive/model/dto/TestCaseDTO.java | 54 ++++++ .../codehive/model/entity/Assignment.java | 165 +++++++++++++++++- .../codehive/model/entity/Execution.java | 115 +++++++++++- .../model/entity/ReferenceSolution.java | 56 +++++- .../codehive/model/entity/Submission.java | 66 +++++++ .../codehive/model/entity/TestCase.java | 75 ++++++++ .../codehive/model/enums/ExecutionStatus.java | 3 +- .../model/mapper/AssignmentMapper.java | 56 ++++++ .../model/mapper/ExecutionMapper.java | 49 ++++++ .../model/mapper/ReferenceSolutionMapper.java | 39 +++++ .../model/mapper/SubmissionMapper.java | 41 +++++ .../codehive/model/mapper/TestCaseMapper.java | 43 +++++ .../repository/AssignmentRepository.java | 21 +++ .../repository/ExecutionRepository.java | 37 ++++ .../ReferenceSolutionRepository.java | 20 +++ .../repository/SubmissionRepository.java | 23 +++ .../repository/TestCaseRepository.java | 24 +++ .../worker/model/enums/ExecutionStatus.java | 3 +- 22 files changed, 1165 insertions(+), 8 deletions(-) create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/dto/AssignmentDTO.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/dto/ExecutionDTO.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/dto/ReferenceSolutionDTO.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/dto/SubmissionDTO.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/dto/TestCaseDTO.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/mapper/AssignmentMapper.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/mapper/ExecutionMapper.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/mapper/ReferenceSolutionMapper.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/mapper/SubmissionMapper.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/mapper/TestCaseMapper.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/repository/AssignmentRepository.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/repository/ExecutionRepository.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/repository/ReferenceSolutionRepository.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/repository/SubmissionRepository.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/repository/TestCaseRepository.java diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/AssignmentDTO.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/AssignmentDTO.java new file mode 100644 index 0000000..291beb5 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/AssignmentDTO.java @@ -0,0 +1,119 @@ +package com.github.codehive.model.dto; + +import java.time.LocalDateTime; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.github.codehive.model.enums.ComparatorType; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class AssignmentDTO { + private Long id; + private String title; + private String description; + private List constraints; + private List hints; + private List tags; + private Integer timeLimitMs; + private Integer memoryLimitMb; + private ComparatorType comparatorType; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private LocalDateTime dueDate; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getConstraints() { + return constraints; + } + + public void setConstraints(List constraints) { + this.constraints = constraints; + } + + public List getHints() { + return hints; + } + + public void setHints(List hints) { + this.hints = hints; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } + + public Integer getTimeLimitMs() { + return timeLimitMs; + } + + public void setTimeLimitMs(Integer timeLimitMs) { + this.timeLimitMs = timeLimitMs; + } + + public Integer getMemoryLimitMb() { + return memoryLimitMb; + } + + public void setMemoryLimitMb(Integer memoryLimitMb) { + this.memoryLimitMb = memoryLimitMb; + } + + public ComparatorType getComparatorType() { + return comparatorType; + } + + public void setComparatorType(ComparatorType comparatorType) { + this.comparatorType = comparatorType; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + public LocalDateTime getDueDate() { + return dueDate; + } + + public void setDueDate(LocalDateTime dueDate) { + this.dueDate = dueDate; + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/ExecutionDTO.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/ExecutionDTO.java new file mode 100644 index 0000000..adec652 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/ExecutionDTO.java @@ -0,0 +1,83 @@ +package com.github.codehive.model.dto; + +import java.time.LocalDateTime; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.github.codehive.model.enums.ExecutionStatus; +import com.github.codehive.model.enums.ExecutionType; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ExecutionDTO { + private Long id; + private Long submissionId; + private ExecutionType executionType; + private ExecutionStatus status; + private Long timeMs; + private Long memoryMb; + private Boolean isOutdated; + private LocalDateTime createdAt; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getSubmissionId() { + return submissionId; + } + + public void setSubmissionId(Long submissionId) { + this.submissionId = submissionId; + } + + public ExecutionType getExecutionType() { + return executionType; + } + + public void setExecutionType(ExecutionType executionType) { + this.executionType = executionType; + } + + public ExecutionStatus getStatus() { + return status; + } + + public void setStatus(ExecutionStatus status) { + this.status = status; + } + + public Long getTimeMs() { + return timeMs; + } + + public void setTimeMs(Long timeMs) { + this.timeMs = timeMs; + } + + public Long getMemoryMb() { + return memoryMb; + } + + public void setMemoryMb(Long memoryMb) { + this.memoryMb = memoryMb; + } + + public Boolean getIsOutdated() { + return isOutdated; + } + + public void setIsOutdated(Boolean isOutdated) { + this.isOutdated = isOutdated; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/ReferenceSolutionDTO.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/ReferenceSolutionDTO.java new file mode 100644 index 0000000..eb8f491 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/ReferenceSolutionDTO.java @@ -0,0 +1,35 @@ +package com.github.codehive.model.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.github.codehive.model.enums.Language; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ReferenceSolutionDTO { + private Long id; + private Long assignmentId; + private Language language; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getAssignmentId() { + return assignmentId; + } + + public void setAssignmentId(Long assignmentId) { + this.assignmentId = assignmentId; + } + + public Language getLanguage() { + return language; + } + + public void setLanguage(Language language) { + this.language = language; + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/SubmissionDTO.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/SubmissionDTO.java new file mode 100644 index 0000000..3f21a16 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/SubmissionDTO.java @@ -0,0 +1,46 @@ +package com.github.codehive.model.dto; + +import java.time.LocalDateTime; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.github.codehive.model.enums.Language; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SubmissionDTO { + private Long id; + private Long assignmentId; + private Language language; + private LocalDateTime createdAt; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getAssignmentId() { + return assignmentId; + } + + public void setAssignmentId(Long assignmentId) { + this.assignmentId = assignmentId; + } + + public Language getLanguage() { + return language; + } + + public void setLanguage(Language language) { + this.language = language; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/TestCaseDTO.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/TestCaseDTO.java new file mode 100644 index 0000000..7063a28 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/TestCaseDTO.java @@ -0,0 +1,54 @@ +package com.github.codehive.model.dto; + +import java.time.LocalDateTime; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class TestCaseDTO { + private Long id; + private Long assignmentId; + private Integer order; + private Boolean isSample; + private LocalDateTime createdAt; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getAssignmentId() { + return assignmentId; + } + + public void setAssignmentId(Long assignmentId) { + this.assignmentId = assignmentId; + } + + public Integer getOrder() { + return order; + } + + public void setOrder(Integer order) { + this.order = order; + } + + public Boolean getIsSample() { + return isSample; + } + + public void setIsSample(Boolean isSample) { + this.isSample = isSample; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/entity/Assignment.java b/codehive-backend/src/main/java/com/github/codehive/model/entity/Assignment.java index 973b79f..139ff31 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/entity/Assignment.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/entity/Assignment.java @@ -1,23 +1,180 @@ package com.github.codehive.model.entity; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; import com.github.codehive.model.enums.ComparatorType; +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Table; + +@Entity +@Table(name = "assignments") public class Assignment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @Column(nullable = false, length = 200) private String title; + + @Column(nullable = false, columnDefinition = "TEXT") private String description; - private List constraints; - private List hints; - private List tags; + + @ElementCollection + @CollectionTable(name = "assignment_constraints", joinColumns = @JoinColumn(name = "assignment_id")) + @Column(name = "constraint_text", columnDefinition = "TEXT") + private List constraints = new ArrayList<>(); + + @ElementCollection + @CollectionTable(name = "assignment_hints", joinColumns = @JoinColumn(name = "assignment_id")) + @Column(name = "hint_text", columnDefinition = "TEXT") + private List hints = new ArrayList<>(); + + @ElementCollection + @CollectionTable(name = "assignment_tags", joinColumns = @JoinColumn(name = "assignment_id")) + @Column(name = "tag", length = 50) + private List tags = new ArrayList<>(); + + @Column(nullable = false) private Integer timeLimitMs; + + @Column(nullable = false) private Integer memoryLimitMb; + + @Column(nullable = false, length = 20) + @Enumerated(EnumType.STRING) private ComparatorType comparatorType; + + @Column(nullable = false) private LocalDateTime createdAt; + + @Column(nullable = false) private LocalDateTime updatedAt; + + @Column(nullable = true) private LocalDateTime dueDate; - + public Assignment() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + this.constraints = new ArrayList<>(); + this.hints = new ArrayList<>(); + this.tags = new ArrayList<>(); + } + + public Assignment(String title, String description, Integer timeLimitMs, Integer memoryLimitMb, ComparatorType comparatorType) { + this(); + this.title = title; + this.description = description; + this.timeLimitMs = timeLimitMs; + this.memoryLimitMb = memoryLimitMb; + this.comparatorType = comparatorType; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getConstraints() { + return constraints; + } + + public void setConstraints(List constraints) { + this.constraints = constraints; + } + + public List getHints() { + return hints; + } + + public void setHints(List hints) { + this.hints = hints; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } + + public Integer getTimeLimitMs() { + return timeLimitMs; + } + + public void setTimeLimitMs(Integer timeLimitMs) { + this.timeLimitMs = timeLimitMs; + } + + public Integer getMemoryLimitMb() { + return memoryLimitMb; + } + + public void setMemoryLimitMb(Integer memoryLimitMb) { + this.memoryLimitMb = memoryLimitMb; + } + + public ComparatorType getComparatorType() { + return comparatorType; + } + + public void setComparatorType(ComparatorType comparatorType) { + this.comparatorType = comparatorType; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + public LocalDateTime getDueDate() { + return dueDate; + } + + public void setDueDate(LocalDateTime dueDate) { + this.dueDate = dueDate; + } } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/entity/Execution.java b/codehive-backend/src/main/java/com/github/codehive/model/entity/Execution.java index d2f5818..f80f995 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/entity/Execution.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/entity/Execution.java @@ -5,14 +5,127 @@ import com.github.codehive.model.enums.ExecutionStatus; import com.github.codehive.model.enums.ExecutionType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +@Entity +@Table(name = "executions") public class Execution { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private Submission submission; // NULLABLE + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "submission_id", nullable = true) + private Submission submission; // NULLABLE for PRACTICE executions + + @Column(nullable = false, length = 20) + @Enumerated(EnumType.STRING) private ExecutionType executionType; + + @Column(nullable = false, length = 20) + @Enumerated(EnumType.STRING) private ExecutionStatus status; + + @Column(nullable = true) private Long timeMs; + + @Column(nullable = true) private Long memoryMb; + + @Column(nullable = false) private Boolean isOutdated; + + @Column(nullable = false) private LocalDateTime createdAt; + + public Execution() { + this.createdAt = LocalDateTime.now(); + this.isOutdated = false; + this.status = ExecutionStatus.PENDING; + } + + public Execution(Submission submission, ExecutionType executionType) { + this(); + this.submission = submission; + this.executionType = executionType; + } + + public Execution(ExecutionType executionType) { + this(); + this.executionType = executionType; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Submission getSubmission() { + return submission; + } + + public void setSubmission(Submission submission) { + this.submission = submission; + } + + public ExecutionType getExecutionType() { + return executionType; + } + + public void setExecutionType(ExecutionType executionType) { + this.executionType = executionType; + } + + public ExecutionStatus getStatus() { + return status; + } + + public void setStatus(ExecutionStatus status) { + this.status = status; + } + + public Long getTimeMs() { + return timeMs; + } + + public void setTimeMs(Long timeMs) { + this.timeMs = timeMs; + } + + public Long getMemoryMb() { + return memoryMb; + } + + public void setMemoryMb(Long memoryMb) { + this.memoryMb = memoryMb; + } + + public Boolean getIsOutdated() { + return isOutdated; + } + + public void setIsOutdated(Boolean isOutdated) { + this.isOutdated = isOutdated; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/entity/ReferenceSolution.java b/codehive-backend/src/main/java/com/github/codehive/model/entity/ReferenceSolution.java index 9586dad..6989e5d 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/entity/ReferenceSolution.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/entity/ReferenceSolution.java @@ -2,8 +2,62 @@ import com.github.codehive.model.enums.Language; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +@Entity +@Table(name = "reference_solutions") public class ReferenceSolution { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "assignment_id", nullable = false) private Assignment assignment; - private Language language; + + @Column(nullable = false, length = 20) + @Enumerated(EnumType.STRING) + private Language language; + + public ReferenceSolution() { + } + + public ReferenceSolution(Assignment assignment, Language language) { + this.assignment = assignment; + this.language = language; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Assignment getAssignment() { + return assignment; + } + + public void setAssignment(Assignment assignment) { + this.assignment = assignment; + } + + public Language getLanguage() { + return language; + } + + public void setLanguage(Language language) { + this.language = language; + } } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/entity/Submission.java b/codehive-backend/src/main/java/com/github/codehive/model/entity/Submission.java index dcffc0f..8525e31 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/entity/Submission.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/entity/Submission.java @@ -4,9 +4,75 @@ import com.github.codehive.model.enums.Language; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +@Entity +@Table(name = "submissions") public class Submission { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "assignment_id", nullable = false) private Assignment assignment; + + @Column(nullable = false, length = 20) + @Enumerated(EnumType.STRING) private Language language; + + @Column(nullable = false) private LocalDateTime createdAt; + + public Submission() { + this.createdAt = LocalDateTime.now(); + } + + public Submission(Assignment assignment, Language language) { + this(); + this.assignment = assignment; + this.language = language; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Assignment getAssignment() { + return assignment; + } + + public void setAssignment(Assignment assignment) { + this.assignment = assignment; + } + + public Language getLanguage() { + return language; + } + + public void setLanguage(Language language) { + this.language = language; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/entity/TestCase.java b/codehive-backend/src/main/java/com/github/codehive/model/entity/TestCase.java index b4afcbd..623b309 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/entity/TestCase.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/entity/TestCase.java @@ -2,10 +2,85 @@ import java.time.LocalDateTime; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +@Entity +@Table(name = "test_cases") public class TestCase { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "assignment_id", nullable = false) private Assignment assignment; + + @Column(nullable = false, name = "order_index") private Integer order; + + @Column(nullable = false) private Boolean isSample; + + @Column(nullable = false) private LocalDateTime createdAt; + + public TestCase() { + this.createdAt = LocalDateTime.now(); + this.isSample = false; + } + + public TestCase(Assignment assignment, Integer order, Boolean isSample) { + this(); + this.assignment = assignment; + this.order = order; + this.isSample = isSample; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Assignment getAssignment() { + return assignment; + } + + public void setAssignment(Assignment assignment) { + this.assignment = assignment; + } + + public Integer getOrder() { + return order; + } + + public void setOrder(Integer order) { + this.order = order; + } + + public Boolean getIsSample() { + return isSample; + } + + public void setIsSample(Boolean isSample) { + this.isSample = isSample; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/enums/ExecutionStatus.java b/codehive-backend/src/main/java/com/github/codehive/model/enums/ExecutionStatus.java index 0c4a848..0a4343f 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/enums/ExecutionStatus.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/enums/ExecutionStatus.java @@ -6,5 +6,6 @@ public enum ExecutionStatus { RTE, // Runtime Error CE, // Compilation Error WA, // Wrong Answer - AC // Accepted + AC, // Accepted + PENDING } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/mapper/AssignmentMapper.java b/codehive-backend/src/main/java/com/github/codehive/model/mapper/AssignmentMapper.java new file mode 100644 index 0000000..c705c2f --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/mapper/AssignmentMapper.java @@ -0,0 +1,56 @@ +package com.github.codehive.model.mapper; + +import java.util.List; + +import com.github.codehive.model.dto.AssignmentDTO; +import com.github.codehive.model.entity.Assignment; + +public class AssignmentMapper { + public static AssignmentDTO toDTO(Assignment assignment) { + if (assignment == null) { + return null; + } + AssignmentDTO dto = new AssignmentDTO(); + dto.setId(assignment.getId()); + dto.setTitle(assignment.getTitle()); + dto.setDescription(assignment.getDescription()); + dto.setConstraints(assignment.getConstraints()); + dto.setHints(assignment.getHints()); + dto.setTags(assignment.getTags()); + dto.setTimeLimitMs(assignment.getTimeLimitMs()); + dto.setMemoryLimitMb(assignment.getMemoryLimitMb()); + dto.setComparatorType(assignment.getComparatorType()); + dto.setCreatedAt(assignment.getCreatedAt()); + dto.setUpdatedAt(assignment.getUpdatedAt()); + dto.setDueDate(assignment.getDueDate()); + return dto; + } + + public static Assignment toEntity(AssignmentDTO dto) { + if (dto == null) { + return null; + } + Assignment assignment = new Assignment(); + assignment.setId(dto.getId()); + assignment.setTitle(dto.getTitle()); + assignment.setDescription(dto.getDescription()); + assignment.setConstraints(dto.getConstraints()); + assignment.setHints(dto.getHints()); + assignment.setTags(dto.getTags()); + assignment.setTimeLimitMs(dto.getTimeLimitMs()); + assignment.setMemoryLimitMb(dto.getMemoryLimitMb()); + assignment.setComparatorType(dto.getComparatorType()); + assignment.setCreatedAt(dto.getCreatedAt()); + assignment.setUpdatedAt(dto.getUpdatedAt()); + assignment.setDueDate(dto.getDueDate()); + return assignment; + } + + public static List toDTOList(List assignments) { + return assignments.stream().map(AssignmentMapper::toDTO).toList(); + } + + public static List toEntityList(List dtos) { + return dtos.stream().map(AssignmentMapper::toEntity).toList(); + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/mapper/ExecutionMapper.java b/codehive-backend/src/main/java/com/github/codehive/model/mapper/ExecutionMapper.java new file mode 100644 index 0000000..a537332 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/mapper/ExecutionMapper.java @@ -0,0 +1,49 @@ +package com.github.codehive.model.mapper; + +import java.util.List; + +import com.github.codehive.model.dto.ExecutionDTO; +import com.github.codehive.model.entity.Execution; + +public class ExecutionMapper { + public static ExecutionDTO toDTO(Execution execution) { + if (execution == null) { + return null; + } + ExecutionDTO dto = new ExecutionDTO(); + dto.setId(execution.getId()); + dto.setSubmissionId(execution.getSubmission() != null ? + execution.getSubmission().getId() : null); + dto.setExecutionType(execution.getExecutionType()); + dto.setStatus(execution.getStatus()); + dto.setTimeMs(execution.getTimeMs()); + dto.setMemoryMb(execution.getMemoryMb()); + dto.setIsOutdated(execution.getIsOutdated()); + dto.setCreatedAt(execution.getCreatedAt()); + return dto; + } + + public static Execution toEntity(ExecutionDTO dto) { + if (dto == null) { + return null; + } + Execution execution = new Execution(); + execution.setId(dto.getId()); + // Note: Submission must be set separately via submission repository + execution.setExecutionType(dto.getExecutionType()); + execution.setStatus(dto.getStatus()); + execution.setTimeMs(dto.getTimeMs()); + execution.setMemoryMb(dto.getMemoryMb()); + execution.setIsOutdated(dto.getIsOutdated()); + execution.setCreatedAt(dto.getCreatedAt()); + return execution; + } + + public static List toDTOList(List executions) { + return executions.stream().map(ExecutionMapper::toDTO).toList(); + } + + public static List toEntityList(List dtos) { + return dtos.stream().map(ExecutionMapper::toEntity).toList(); + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/mapper/ReferenceSolutionMapper.java b/codehive-backend/src/main/java/com/github/codehive/model/mapper/ReferenceSolutionMapper.java new file mode 100644 index 0000000..8f4016f --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/mapper/ReferenceSolutionMapper.java @@ -0,0 +1,39 @@ +package com.github.codehive.model.mapper; + +import java.util.List; + +import com.github.codehive.model.dto.ReferenceSolutionDTO; +import com.github.codehive.model.entity.ReferenceSolution; + +public class ReferenceSolutionMapper { + public static ReferenceSolutionDTO toDTO(ReferenceSolution referenceSolution) { + if (referenceSolution == null) { + return null; + } + ReferenceSolutionDTO dto = new ReferenceSolutionDTO(); + dto.setId(referenceSolution.getId()); + dto.setAssignmentId(referenceSolution.getAssignment() != null ? + referenceSolution.getAssignment().getId() : null); + dto.setLanguage(referenceSolution.getLanguage()); + return dto; + } + + public static ReferenceSolution toEntity(ReferenceSolutionDTO dto) { + if (dto == null) { + return null; + } + ReferenceSolution referenceSolution = new ReferenceSolution(); + referenceSolution.setId(dto.getId()); + // Note: Assignment must be set separately via assignment repository + referenceSolution.setLanguage(dto.getLanguage()); + return referenceSolution; + } + + public static List toDTOList(List referenceSolutions) { + return referenceSolutions.stream().map(ReferenceSolutionMapper::toDTO).toList(); + } + + public static List toEntityList(List dtos) { + return dtos.stream().map(ReferenceSolutionMapper::toEntity).toList(); + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/mapper/SubmissionMapper.java b/codehive-backend/src/main/java/com/github/codehive/model/mapper/SubmissionMapper.java new file mode 100644 index 0000000..c924475 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/mapper/SubmissionMapper.java @@ -0,0 +1,41 @@ +package com.github.codehive.model.mapper; + +import java.util.List; + +import com.github.codehive.model.dto.SubmissionDTO; +import com.github.codehive.model.entity.Submission; + +public class SubmissionMapper { + public static SubmissionDTO toDTO(Submission submission) { + if (submission == null) { + return null; + } + SubmissionDTO dto = new SubmissionDTO(); + dto.setId(submission.getId()); + dto.setAssignmentId(submission.getAssignment() != null ? + submission.getAssignment().getId() : null); + dto.setLanguage(submission.getLanguage()); + dto.setCreatedAt(submission.getCreatedAt()); + return dto; + } + + public static Submission toEntity(SubmissionDTO dto) { + if (dto == null) { + return null; + } + Submission submission = new Submission(); + submission.setId(dto.getId()); + // Note: Assignment must be set separately via assignment repository + submission.setLanguage(dto.getLanguage()); + submission.setCreatedAt(dto.getCreatedAt()); + return submission; + } + + public static List toDTOList(List submissions) { + return submissions.stream().map(SubmissionMapper::toDTO).toList(); + } + + public static List toEntityList(List dtos) { + return dtos.stream().map(SubmissionMapper::toEntity).toList(); + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/mapper/TestCaseMapper.java b/codehive-backend/src/main/java/com/github/codehive/model/mapper/TestCaseMapper.java new file mode 100644 index 0000000..2902a59 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/mapper/TestCaseMapper.java @@ -0,0 +1,43 @@ +package com.github.codehive.model.mapper; + +import java.util.List; + +import com.github.codehive.model.dto.TestCaseDTO; +import com.github.codehive.model.entity.TestCase; + +public class TestCaseMapper { + public static TestCaseDTO toDTO(TestCase testCase) { + if (testCase == null) { + return null; + } + TestCaseDTO dto = new TestCaseDTO(); + dto.setId(testCase.getId()); + dto.setAssignmentId(testCase.getAssignment() != null ? + testCase.getAssignment().getId() : null); + dto.setOrder(testCase.getOrder()); + dto.setIsSample(testCase.getIsSample()); + dto.setCreatedAt(testCase.getCreatedAt()); + return dto; + } + + public static TestCase toEntity(TestCaseDTO dto) { + if (dto == null) { + return null; + } + TestCase testCase = new TestCase(); + testCase.setId(dto.getId()); + // Note: Assignment must be set separately via assignment repository + testCase.setOrder(dto.getOrder()); + testCase.setIsSample(dto.getIsSample()); + testCase.setCreatedAt(dto.getCreatedAt()); + return testCase; + } + + public static List toDTOList(List testCases) { + return testCases.stream().map(TestCaseMapper::toDTO).toList(); + } + + public static List toEntityList(List dtos) { + return dtos.stream().map(TestCaseMapper::toEntity).toList(); + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/repository/AssignmentRepository.java b/codehive-backend/src/main/java/com/github/codehive/repository/AssignmentRepository.java new file mode 100644 index 0000000..3b728c6 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/repository/AssignmentRepository.java @@ -0,0 +1,21 @@ +package com.github.codehive.repository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.github.codehive.model.entity.Assignment; + +public interface AssignmentRepository extends JpaRepository { + List findAllByOrderByCreatedAtDesc(); + + List findByTitleContainingIgnoreCase(String title); + + List findByDueDateBefore(LocalDateTime date); + + List findByDueDateAfter(LocalDateTime date); + + Optional findByIdAndDueDateAfter(Long id, LocalDateTime date); +} diff --git a/codehive-backend/src/main/java/com/github/codehive/repository/ExecutionRepository.java b/codehive-backend/src/main/java/com/github/codehive/repository/ExecutionRepository.java new file mode 100644 index 0000000..f76aa03 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/repository/ExecutionRepository.java @@ -0,0 +1,37 @@ +package com.github.codehive.repository; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.github.codehive.model.entity.Execution; +import com.github.codehive.model.entity.Submission; +import com.github.codehive.model.enums.ExecutionStatus; +import com.github.codehive.model.enums.ExecutionType; + +public interface ExecutionRepository extends JpaRepository { + List findBySubmission(Submission submission); + + List findBySubmissionId(Long submissionId); + + List findBySubmissionIdOrderByCreatedAtDesc(Long submissionId); + + List findByExecutionType(ExecutionType executionType); + + List findByStatus(ExecutionStatus status); + + List findByExecutionTypeAndStatus(ExecutionType executionType, ExecutionStatus status); + + List findByIsOutdated(Boolean isOutdated); + + List findByCreatedAtBefore(LocalDateTime date); + + List findByCreatedAtAfter(LocalDateTime date); + + List findBySubmissionIsNull(); + + long countBySubmissionId(Long submissionId); + + long countByStatus(ExecutionStatus status); +} diff --git a/codehive-backend/src/main/java/com/github/codehive/repository/ReferenceSolutionRepository.java b/codehive-backend/src/main/java/com/github/codehive/repository/ReferenceSolutionRepository.java new file mode 100644 index 0000000..a080fcf --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/repository/ReferenceSolutionRepository.java @@ -0,0 +1,20 @@ +package com.github.codehive.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.github.codehive.model.entity.Assignment; +import com.github.codehive.model.entity.ReferenceSolution; +import com.github.codehive.model.enums.Language; + +public interface ReferenceSolutionRepository extends JpaRepository { + List findByAssignment(Assignment assignment); + + Optional findByAssignmentAndLanguage(Assignment assignment, Language language); + + List findByAssignmentId(Long assignmentId); + + boolean existsByAssignmentAndLanguage(Assignment assignment, Language language); +} diff --git a/codehive-backend/src/main/java/com/github/codehive/repository/SubmissionRepository.java b/codehive-backend/src/main/java/com/github/codehive/repository/SubmissionRepository.java new file mode 100644 index 0000000..1175ed8 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/repository/SubmissionRepository.java @@ -0,0 +1,23 @@ +package com.github.codehive.repository; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.github.codehive.model.entity.Assignment; +import com.github.codehive.model.entity.Submission; + +public interface SubmissionRepository extends JpaRepository { + List findByAssignment(Assignment assignment); + + List findByAssignmentId(Long assignmentId); + + List findByAssignmentIdOrderByCreatedAtDesc(Long assignmentId); + + List findByCreatedAtBefore(LocalDateTime date); + + List findByCreatedAtAfter(LocalDateTime date); + + long countByAssignmentId(Long assignmentId); +} diff --git a/codehive-backend/src/main/java/com/github/codehive/repository/TestCaseRepository.java b/codehive-backend/src/main/java/com/github/codehive/repository/TestCaseRepository.java new file mode 100644 index 0000000..c8c9e72 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/repository/TestCaseRepository.java @@ -0,0 +1,24 @@ +package com.github.codehive.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.github.codehive.model.entity.Assignment; +import com.github.codehive.model.entity.TestCase; + +public interface TestCaseRepository extends JpaRepository { + List findByAssignment(Assignment assignment); + + List findByAssignmentId(Long assignmentId); + + List findByAssignmentOrderByOrderAsc(Assignment assignment); + + List findByAssignmentIdOrderByOrderAsc(Long assignmentId); + + List findByAssignmentAndIsSample(Assignment assignment, Boolean isSample); + + List findByAssignmentIdAndIsSample(Long assignmentId, Boolean isSample); + + long countByAssignmentId(Long assignmentId); +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/model/enums/ExecutionStatus.java b/codehive-worker/src/main/java/com/github/codehive/worker/model/enums/ExecutionStatus.java index 2df58b4..dd367c4 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/model/enums/ExecutionStatus.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/model/enums/ExecutionStatus.java @@ -6,5 +6,6 @@ public enum ExecutionStatus { RTE, // Runtime Error CE, // Compilation Error WA, // Wrong Answer - AC // Accepted + AC, // Accepted + PENDING } From 1576d00e339e2a9154b03444bdb86faa470473db Mon Sep 17 00:00:00 2001 From: IrminDev Date: Thu, 22 Jan 2026 20:44:48 -0600 Subject: [PATCH 07/32] Enhanced the worker response --- .../worker/model/dto/TestCaseResult.java | 33 +----- .../worker/sandbox/java/JavaExecutor.java | 9 +- .../service/OutputComparatorService.java | 107 ++++++++++++++---- .../worker/service/TestExecutionService.java | 94 +++++++++------ .../worker/util/ObjectKeyBuilder.java | 31 ----- 5 files changed, 149 insertions(+), 125 deletions(-) delete mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/util/ObjectKeyBuilder.java diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/TestCaseResult.java b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/TestCaseResult.java index a8b7df3..b49b65c 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/TestCaseResult.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/TestCaseResult.java @@ -5,9 +5,6 @@ public class TestCaseResult { private int testCaseNumber; private ExecutionStatus status; - private String output; - private String expectedOutput; - private String errorOutput; private Long executionTimeMs; private Long memoryUsedKb; private String feedback; @@ -15,12 +12,10 @@ public class TestCaseResult { public TestCaseResult() { } - public TestCaseResult(int testCaseNumber, ExecutionStatus status, String output, - String expectedOutput, Long executionTimeMs, Long memoryUsedKb) { + public TestCaseResult(int testCaseNumber, ExecutionStatus status, + Long executionTimeMs, Long memoryUsedKb) { this.testCaseNumber = testCaseNumber; this.status = status; - this.output = output; - this.expectedOutput = expectedOutput; this.executionTimeMs = executionTimeMs; this.memoryUsedKb = memoryUsedKb; } @@ -42,30 +37,6 @@ public void setStatus(ExecutionStatus status) { this.status = status; } - public String getOutput() { - return output; - } - - public void setOutput(String output) { - this.output = output; - } - - public String getExpectedOutput() { - return expectedOutput; - } - - public void setExpectedOutput(String expectedOutput) { - this.expectedOutput = expectedOutput; - } - - public String getErrorOutput() { - return errorOutput; - } - - public void setErrorOutput(String errorOutput) { - this.errorOutput = errorOutput; - } - public Long getExecutionTimeMs() { return executionTimeMs; } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java index fe488ea..8004460 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java @@ -3,9 +3,11 @@ import com.github.codehive.worker.sandbox.ExecutionResult; import com.github.codehive.worker.sandbox.LanguageExecutor; import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.async.ResultCallback.Adapter; import com.github.dockerjava.api.command.CreateContainerResponse; import com.github.dockerjava.api.command.WaitContainerResultCallback; import com.github.dockerjava.api.model.Bind; +import com.github.dockerjava.api.model.Frame; import com.github.dockerjava.api.model.HostConfig; import com.github.dockerjava.api.model.Volume; import org.slf4j.Logger; @@ -186,8 +188,7 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit if (exitCode != 0) { return ExecutionResult.runtimeError(stderr, exitCode, executionTime); } - - // TODO: Compare output with expected output for AC/WA verdict + return ExecutionResult.success(stdout, executionTime, 0L); } catch (Exception e) { @@ -210,9 +211,9 @@ private String getContainerLogs(String containerId, boolean stderr) { dockerClient.logContainerCmd(containerId) .withStdOut(!stderr) .withStdErr(stderr) - .exec(new com.github.dockerjava.api.async.ResultCallback.Adapter() { + .exec(new Adapter() { @Override - public void onNext(com.github.dockerjava.api.model.Frame frame) { + public void onNext(Frame frame) { try { outputStream.write(frame.getPayload()); } catch (Exception e) { diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/service/OutputComparatorService.java b/codehive-worker/src/main/java/com/github/codehive/worker/service/OutputComparatorService.java index 5208b86..c6298b7 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/service/OutputComparatorService.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/service/OutputComparatorService.java @@ -14,68 +14,106 @@ public class OutputComparatorService { private static final Logger logger = LoggerFactory.getLogger(OutputComparatorService.class); private static final double EPSILON = 1e-9; + public static class ComparisonResult { + private final boolean matches; + private final String feedback; + + public ComparisonResult(boolean matches, String feedback) { + this.matches = matches; + this.feedback = feedback; + } + + public boolean matches() { + return matches; + } + + public String getFeedback() { + return feedback; + } + } + /** * Compare two outputs based on the comparator type * @param expected Expected output * @param actual Actual output from execution * @param comparatorType Type of comparison to perform - * @return true if outputs match, false otherwise + * @return ComparisonResult with match status and detailed feedback */ - public boolean compare(String expected, String actual, ComparatorType comparatorType) { + public ComparisonResult compareWithFeedback(String expected, String actual, ComparatorType comparatorType) { if (expected == null && actual == null) { - return true; + return new ComparisonResult(true, "Passed"); } if (expected == null || actual == null) { - return false; + return new ComparisonResult(false, "Output is null"); } switch (comparatorType) { case EXACT_MATCH: - return compareExact(expected, actual); + return compareExactWithFeedback(expected, actual); case FLOATING_POINT: - return compareFloatingPoint(expected, actual); + return compareFloatingPointWithFeedback(expected, actual); default: logger.warn("Unknown comparator type: {}, falling back to exact match", comparatorType); - return compareExact(expected, actual); + return compareExactWithFeedback(expected, actual); } } /** - * Exact string comparison (trimmed lines, ignoring trailing whitespace) + * Compare two outputs based on the comparator type (legacy method) + * @param expected Expected output + * @param actual Actual output from execution + * @param comparatorType Type of comparison to perform + * @return true if outputs match, false otherwise */ - private boolean compareExact(String expected, String actual) { + public boolean compare(String expected, String actual, ComparatorType comparatorType) { + return compareWithFeedback(expected, actual, comparatorType).matches(); + } + + /** + * Exact string comparison with detailed feedback + */ + private ComparisonResult compareExactWithFeedback(String expected, String actual) { List expectedLines = normalizeLines(expected); List actualLines = normalizeLines(actual); if (expectedLines.size() != actualLines.size()) { - logger.debug("Line count mismatch: expected {} lines, got {} lines", + String feedback = String.format("Line count mismatch: expected %d lines, got %d lines", expectedLines.size(), actualLines.size()); - return false; + logger.debug(feedback); + return new ComparisonResult(false, feedback); } for (int i = 0; i < expectedLines.size(); i++) { if (!expectedLines.get(i).equals(actualLines.get(i))) { - logger.debug("Line {} mismatch: expected '{}', got '{}'", - i + 1, expectedLines.get(i), actualLines.get(i)); - return false; + String feedback = String.format("Line %d mismatch: expected '%s', got '%s'", + i + 1, truncate(expectedLines.get(i), 50), truncate(actualLines.get(i), 50)); + logger.debug(feedback); + return new ComparisonResult(false, feedback); } } - return true; + return new ComparisonResult(true, "Passed"); } /** - * Floating point comparison with tolerance - * Compares token by token, treating numbers with epsilon tolerance + * Exact string comparison (trimmed lines, ignoring trailing whitespace) */ - private boolean compareFloatingPoint(String expected, String actual) { + private boolean compareExact(String expected, String actual) { + return compareExactWithFeedback(expected, actual).matches(); + } + + /** + * Floating point comparison with tolerance and detailed feedback + */ + private ComparisonResult compareFloatingPointWithFeedback(String expected, String actual) { List expectedLines = normalizeLines(expected); List actualLines = normalizeLines(actual); if (expectedLines.size() != actualLines.size()) { - logger.debug("Line count mismatch: expected {} lines, got {} lines", + String feedback = String.format("Line count mismatch: expected %d lines, got %d lines", expectedLines.size(), actualLines.size()); - return false; + logger.debug(feedback); + return new ComparisonResult(false, feedback); } for (int i = 0; i < expectedLines.size(); i++) { @@ -83,21 +121,40 @@ private boolean compareFloatingPoint(String expected, String actual) { String[] actualTokens = actualLines.get(i).split("\\s+"); if (expectedTokens.length != actualTokens.length) { - logger.debug("Token count mismatch on line {}: expected {} tokens, got {} tokens", + String feedback = String.format("Token count mismatch on line %d: expected %d tokens, got %d tokens", i + 1, expectedTokens.length, actualTokens.length); - return false; + logger.debug(feedback); + return new ComparisonResult(false, feedback); } for (int j = 0; j < expectedTokens.length; j++) { if (!compareTokens(expectedTokens[j], actualTokens[j])) { - logger.debug("Token mismatch at line {} position {}: expected '{}', got '{}'", + String feedback = String.format("Token mismatch at line %d position %d: expected '%s', got '%s'", i + 1, j + 1, expectedTokens[j], actualTokens[j]); - return false; + logger.debug(feedback); + return new ComparisonResult(false, feedback); } } } - return true; + return new ComparisonResult(true, "Passed"); + } + + /** + * Floating point comparison with tolerance + * Compares token by token, treating numbers with epsilon tolerance + */ + private boolean compareFloatingPoint(String expected, String actual) { + return compareFloatingPointWithFeedback(expected, actual).matches(); + } + + /** + * Truncate string for display in feedback + */ + private String truncate(String str, int maxLength) { + if (str == null) return "null"; + if (str.length() <= maxLength) return str; + return str.substring(0, maxLength) + "..."; } /** diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java b/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java index 9310373..5da4166 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java @@ -4,7 +4,6 @@ import com.github.codehive.worker.model.dto.ExecutionReport; import com.github.codehive.worker.model.dto.TestCaseResult; import com.github.codehive.worker.model.dto.queue.ExecutionJob; -import com.github.codehive.worker.model.enums.ComparatorType; import com.github.codehive.worker.model.enums.ExecutionStatus; import com.github.codehive.worker.model.enums.ExecutionType; import com.github.codehive.worker.sandbox.ExecutionResult; @@ -43,9 +42,6 @@ public ExecutionReport executeJob(ExecutionJob job) { ExecutionReport report = new ExecutionReport(job.getId()); try { - // Download source code - InputStream sourceCode = objectStorageService.download(job.getSource()); - // Get executor LanguageExecutor executor = executorFactory.getExecutor(job.getLanguage()); @@ -113,31 +109,31 @@ private void executeDefinitiveTests(ExecutionJob job, LanguageExecutor executor, job.getMemoryLimitMb() ); - // Create test case result + // Upload stdout and stderr to MinIO + uploadTestCaseOutputs(job.getOutputPath(), i, result.getOutput(), result.getErrorOutput()); + + // Create test case result (without output/errorOutput/expectedOutput) TestCaseResult testResult = new TestCaseResult( i, result.getStatus(), - result.getOutput(), - expectedOutput, result.getExecutionTimeMs(), result.getMemoryUsedKb() ); - testResult.setErrorOutput(result.getErrorOutput()); - // Compare outputs if execution was successful if (result.getStatus() == ExecutionStatus.AC) { - boolean matches = outputComparatorService.compare( - expectedOutput, - result.getOutput(), - job.getComparatorType() - ); + OutputComparatorService.ComparisonResult comparison = + outputComparatorService.compareWithFeedback( + expectedOutput, + result.getOutput(), + job.getComparatorType() + ); - if (!matches) { + if (!comparison.matches()) { testResult.setStatus(ExecutionStatus.WA); - testResult.setFeedback("Output does not match expected output"); + testResult.setFeedback(comparison.getFeedback()); } else { - testResult.setFeedback("Test passed"); + testResult.setFeedback("Passed"); } } else { testResult.setFeedback(getStatusFeedback(result.getStatus())); @@ -168,7 +164,7 @@ private void executePracticeTests(ExecutionJob job, LanguageExecutor executor, E for (int i = 0; i < job.getTestCases().size(); i++) { try { String testInput = job.getTestCases().get(i); - InputStream testInputStream = new ByteArrayInputStream(testInput.getBytes(StandardCharsets.UTF_8)); + int testNumber = i + 1; // Execute reference solution to get expected output ExecutionResult referenceResult = referenceExecutor.execute( @@ -179,9 +175,9 @@ private void executePracticeTests(ExecutionJob job, LanguageExecutor executor, E ); if (referenceResult.getStatus() != ExecutionStatus.AC) { - logger.error("Reference solution failed on test case {}: {}", i + 1, referenceResult.getStatus()); + logger.error("Reference solution failed on test case {}: {}", testNumber, referenceResult.getStatus()); TestCaseResult errorResult = new TestCaseResult(); - errorResult.setTestCaseNumber(i + 1); + errorResult.setTestCaseNumber(testNumber); errorResult.setStatus(ExecutionStatus.RTE); errorResult.setFeedback("Reference solution failed"); report.addTestCaseResult(errorResult); @@ -198,31 +194,31 @@ private void executePracticeTests(ExecutionJob job, LanguageExecutor executor, E job.getMemoryLimitMb() ); - // Create test case result + // Upload stdout and stderr to MinIO + uploadTestCaseOutputs(job.getOutputPath(), testNumber, result.getOutput(), result.getErrorOutput()); + + // Create test case result (without output/errorOutput/expectedOutput) TestCaseResult testResult = new TestCaseResult( - i + 1, + testNumber, result.getStatus(), - result.getOutput(), - expectedOutput, result.getExecutionTimeMs(), result.getMemoryUsedKb() ); - testResult.setErrorOutput(result.getErrorOutput()); - // Compare outputs if execution was successful if (result.getStatus() == ExecutionStatus.AC) { - boolean matches = outputComparatorService.compare( - expectedOutput, - result.getOutput(), - job.getComparatorType() - ); + OutputComparatorService.ComparisonResult comparison = + outputComparatorService.compareWithFeedback( + expectedOutput, + result.getOutput(), + job.getComparatorType() + ); - if (!matches) { + if (!comparison.matches()) { testResult.setStatus(ExecutionStatus.WA); - testResult.setFeedback("Output does not match expected output"); + testResult.setFeedback(comparison.getFeedback()); } else { - testResult.setFeedback("Test passed"); + testResult.setFeedback("Passed"); } } else { testResult.setFeedback(getStatusFeedback(result.getStatus())); @@ -241,6 +237,36 @@ private void executePracticeTests(ExecutionJob job, LanguageExecutor executor, E } } + /** + * Upload test case stdout and stderr to MinIO + */ + private void uploadTestCaseOutputs(String path, int testCaseNumber, String stdout, String stderr) { + try { + // Upload stdout + String stdoutPath = new StringBuilder() + .append(path) + .append("/tc-") + .append(testCaseNumber) + .append("/stdout.txt") + .toString(); + objectStorageService.upload(stdoutPath, stdout != null ? stdout : ""); + logger.debug("Uploaded stdout for test case {} to: {}", testCaseNumber, stdoutPath); + + // Upload stderr + String stderrPath = new StringBuilder() + .append(path) + .append("/tc-") + .append(testCaseNumber) + .append("/stderr.txt") + .toString(); + objectStorageService.upload(stderrPath, stderr != null ? stderr : ""); + logger.debug("Uploaded stderr for test case {} to: {}", testCaseNumber, stderrPath); + + } catch (Exception e) { + logger.error("Failed to upload test case outputs for test {}", testCaseNumber, e); + } + } + private String getStatusFeedback(ExecutionStatus status) { switch (status) { case TLE: diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/util/ObjectKeyBuilder.java b/codehive-worker/src/main/java/com/github/codehive/worker/util/ObjectKeyBuilder.java deleted file mode 100644 index 246bc3f..0000000 --- a/codehive-worker/src/main/java/com/github/codehive/worker/util/ObjectKeyBuilder.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.github.codehive.worker.util; - -public class ObjectKeyBuilder { - public static String testCaseInput(Long assignmentId, Long testCaseId) { - return String.format("test-suites/assignments/%d/tc-%d/tc%d.in", assignmentId, testCaseId, testCaseId); - } - - public static String testCaseOutput(Long assignmentId, Long testCaseId) { - return String.format("test-suites/assignments/%d/tc-%d/tc%d.out", assignmentId, testCaseId, testCaseId); - } - - public static String submissionSourceCode(Long assignmentId, Long submissionId, String fileExtension, Long groupId) { - return String.format("submissions/groups/%d/assignments/%d/submission-%d/Main.%s", groupId, assignmentId, submissionId, fileExtension); - } - - public static String executionOutput(Long submissionId, Long executionId, Long groupId, String fileExtension) { - return String.format("executions/groups/%d/assignments/%d/execution-%d/output.%s", groupId, submissionId, executionId, fileExtension); - } - - public static String referenceSolutionSourceCode(Long assignmentId, Long referenceSolutionId, String fileExtension) { - return String.format("reference-solutions/assignments/%d/reference-solution-%d/Main.%s", assignmentId, referenceSolutionId, fileExtension); - } - - public static String executionTestCaseOutput(Long executionId, String fileExtension) { - return String.format("test-execution/execution-%d/output.%s", executionId, fileExtension); - } - - public static String executionTestSource(Long executionId, String fileExtension) { - return String.format("test-execution/execution-%d/Main.%s", executionId, fileExtension); - } -} From 4bef89557533edb6fa63b9ce749f187783089706 Mon Sep 17 00:00:00 2001 From: IrminDev Date: Fri, 23 Jan 2026 22:48:47 -0600 Subject: [PATCH 08/32] Added code execution functionality in the backend, added logs for better understanding in the workflow and fixed business logic --- AGENTS.md | 124 ++++++++++++++-- .../github/codehive/config/MinioConfig.java | 16 +- .../github/codehive/config/RabbitConfig.java | 16 +- .../codehive/config/SecurityConfig.java | 3 +- .../controller/CheckExecutionController.java | 117 +++++++++++++++ .../listener/ExecutionResultListener.java | 37 +++++ .../producer/ExecutionRequestProducer.java | 37 +++++ .../codehive/model/dto/ExecutionDTO.java | 9 ++ .../model/dto/queue/ExecutionReport.java | 108 ++++++++++++++ .../model/dto/queue/TestCaseResult.java | 62 ++++++++ .../codehive/model/entity/Execution.java | 23 ++- .../model/mapper/ExecutionMapper.java | 4 +- .../request/execution/ExecutionRequest.java | 87 +++++++++++ .../codehive/service/ExecutionProducer.java | 23 --- .../service/ExecutionRequestService.java | 140 ++++++++++++++++++ .../service/ExecutionResultService.java | 61 ++++++++ .../service/ObjectStorageService.java | 23 ++- .../codehive/utils/FileExtensionUtil.java | 20 +++ .../codehive/utils/ObjectKeyBuilder.java | 17 ++- .../src/main/resources/application.properties | 44 +++--- .../codehive/worker/config/MinioConfig.java | 16 +- .../worker/config/RabbitMQConfig.java | 6 + .../listener/SubmissionListener.java | 36 ++++- .../producer/ExecutionResultProducer.java | 28 ++++ .../worker/sandbox/SandboxExecutor.java | 5 - .../worker/sandbox/java/JavaExecutor.java | 2 +- .../worker/service/ObjectStorageService.java | 6 +- .../service/OutputComparatorService.java | 14 -- .../worker/service/TestExecutionService.java | 24 +-- .../src/main/resources/application.properties | 20 +-- 30 files changed, 1009 insertions(+), 119 deletions(-) create mode 100644 codehive-backend/src/main/java/com/github/codehive/controller/CheckExecutionController.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/messaging/listener/ExecutionResultListener.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/messaging/producer/ExecutionRequestProducer.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/dto/queue/ExecutionReport.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/dto/queue/TestCaseResult.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/request/execution/ExecutionRequest.java delete mode 100644 codehive-backend/src/main/java/com/github/codehive/service/ExecutionProducer.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/service/ExecutionRequestService.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/service/ExecutionResultService.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/utils/FileExtensionUtil.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/messaging/producer/ExecutionResultProducer.java delete mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/sandbox/SandboxExecutor.java diff --git a/AGENTS.md b/AGENTS.md index 36ea4c5..2673bc8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,8 +18,10 @@ Current development focuses on authentication, user management, and the foundati CodeHive/ ├── codehive-backend/ # Main Spring Boot API │ ├── src/main/java/com/github/codehive/ -│ │ ├── config/ # Spring configuration (Security, OpenAPI, Cache, etc.) +│ │ ├── config/ # Spring configuration (Security, OpenAPI, Cache, RabbitMQ) │ │ ├── controller/ # REST controllers (Auth, RecoveryPassword) +│ │ ├── messaging/ +│ │ │ └── listener/ # RabbitMQ message listeners (ExecutionResultListener) │ │ ├── model/ │ │ │ ├── entity/ # JPA entities (User, PasswordResetToken) │ │ │ ├── dto/ # Data Transfer Objects (UserDTO) @@ -42,7 +44,8 @@ CodeHive/ │ ├── src/main/java/com/github/codehive/worker/ │ │ ├── config/ # RabbitMQ, Docker, and MinIO configuration │ │ ├── messaging/ -│ │ │ └── listener/ # RabbitMQ message listeners (SubmissionListener) +│ │ │ ├── listener/ # RabbitMQ message listeners (SubmissionListener) +│ │ │ └── producer/ # RabbitMQ message producers (ExecutionResultProducer) │ │ ├── model/ │ │ │ ├── dto/queue/ # ExecutionJob DTO │ │ │ └── enums/ # Language, ExecutionStatus, ExecutionType, ComparatorType @@ -350,6 +353,73 @@ public @interface RateLimit { 3. **Aspect intercepts** and checks `RateLimitService` (IP-based buckets) +#### RabbitMQ Messaging + +**Backend RabbitMQ Configuration** (`config/RabbitConfig.java`): +```java +@Configuration +public class RabbitConfig { + public static final String QUEUE_NAME = "codehive_queue"; // For sending jobs to worker + public static final String RESULT_QUEUE_NAME = "codehive_result_queue"; // For receiving results + + @Bean + Queue executionQueue() { return new Queue(QUEUE_NAME, true); } + + @Bean + Queue resultQueue() { return new Queue(RESULT_QUEUE_NAME, true); } + + @Bean + public MessageConverter jsonMessageConverter() { + return new Jackson2JsonMessageConverter(); + } +} +``` + +**Execution Result Listener** (`messaging/listener/ExecutionResultListener.java`): +```java +@Component +public class ExecutionResultListener { + private final ExecutionResultService executionResultService; + + @RabbitListener(queues = "${rabbitmq.result.queue:codehive_result_queue}") + public void handleExecutionResult(ExecutionReport report) { + logger.info("Received execution result: executionId={}, status={}", + report.getExecutionId(), report.getOverallStatus()); + executionResultService.processExecutionResult(report); + } +} +``` + +**Execution Result Service** (`service/ExecutionResultService.java`): +```java +@Service +public class ExecutionResultService { + private final ExecutionRepository executionRepository; + + @Transactional + public void processExecutionResult(ExecutionReport report) { + Execution execution = executionRepository.findById(report.getExecutionId()) + .orElse(null); + + if (execution == null) { + logger.warn("Execution not found: id={}", report.getExecutionId()); + return; + } + + execution.setStatus(report.getOverallStatus()); + execution.setTimeMs(report.getMaxExecutionTimeMs()); + execution.setMemoryMb(report.getMaxMemoryUsedKb() / 1024); // KB to MB + + executionRepository.save(execution); + } +} +``` + +**Queue DTOs** (`model/dto/queue/`): +- `ExecutionJob.java`: Sent to worker (source path, language, test config, limits) +- `ExecutionReport.java`: Received from worker (status, test results, statistics) +- `TestCaseResult.java`: Individual test case result (status, time, memory, feedback) + #### Testing Conventions **Unit Tests** (`*Test.java`): @@ -392,7 +462,9 @@ class AuthServiceTest { - **config/**: Spring configuration beans (RabbitMQ, Docker client, MinIO) - **messaging/**: RabbitMQ message handling - **listener/**: `@RabbitListener` components (SubmissionListener) + - **producer/**: Message producers (ExecutionResultProducer) - **model/**: Data models + - **dto/**: DTOs (ExecutionReport, TestCaseResult) - **dto/queue/**: Message DTOs (ExecutionJob) - **enums/**: Enums (Language, ExecutionStatus, ExecutionType, ComparatorType) - **sandbox/**: Code execution in Docker containers @@ -522,6 +594,7 @@ public class ExecutionJob { @Component public class SubmissionListener { private final TestExecutionService testExecutionService; + private final ExecutionResultProducer executionResultProducer; @RabbitListener(queues = "${rabbitmq.queue:codehive_queue}") public void handleExecutionJob(ExecutionJob job) { @@ -538,7 +611,23 @@ public class SubmissionListener { job.getId(), report.getOverallStatus(), report.getPassedTests(), report.getTotalTests()); - // TODO: Send result back to backend via RabbitMQ result queue + // Send result back to backend via RabbitMQ result queue + executionResultProducer.sendExecutionResult(report); + } +} +``` + +**Producer** (`messaging/producer/ExecutionResultProducer.java`): +```java +@Service +public class ExecutionResultProducer { + private final RabbitTemplate rabbitTemplate; + + public void sendExecutionResult(ExecutionReport report) { + logger.info("Sending execution result: executionId={}, status={}", + report.getExecutionId(), report.getOverallStatus()); + + rabbitTemplate.convertAndSend(RabbitMQConfig.RESULT_QUEUE_NAME, report); } } ``` @@ -579,11 +668,17 @@ The service orchestrates test execution based on execution type: @Configuration public class RabbitMQConfig { public static final String QUEUE_NAME = System.getProperty("rabbitmq.queue", "codehive_queue"); + public static final String RESULT_QUEUE_NAME = System.getProperty("rabbitmq.result.queue", "codehive_result_queue"); @Bean Queue executionQueue() { return new Queue(QUEUE_NAME, true); // Durable queue } + + @Bean + Queue resultQueue() { + return new Queue(RESULT_QUEUE_NAME, true); // Durable queue for results + } @Bean public MessageConverter jsonMessageConverter() { @@ -645,7 +740,8 @@ public class ObjectStorageService { - Exit code 0 → Return AC (Accepted) or WA (Wrong Answer, TODO) 8. **Result collection** → Capture stdout, stderr, exit code, execution time 9. **Cleanup** → Remove Docker container, delete temp files -10. **Result publishing** → TODO: Send `ExecutionResult` back to backend via RabbitMQ +10. **Result publishing** → `ExecutionResultProducer` sends `ExecutionReport` to `codehive_result_queue` +11. **Backend processing** → `ExecutionResultListener` receives report, `ExecutionResultService` updates `Execution` entity **Docker Images**: - Java: `openjdk:21-slim` (compilation: `javac`, execution: `java`) @@ -1171,7 +1267,10 @@ void login_WithWrongPassword_ThrowsIncorrectCredentialsException() { ### Infrastructure -1. **RabbitMQ Integration**: ✅ **IMPLEMENTED** - Worker service consumes code execution jobs from RabbitMQ queue (`codehive_queue`). Backend sends `ExecutionJob` messages, worker processes them and returns results (result publishing TODO). +1. **RabbitMQ Integration**: ✅ **FULLY IMPLEMENTED** - Bidirectional communication between backend and worker: + - Backend sends `ExecutionJob` to `codehive_queue` via `ExecutionProducer` + - Worker processes jobs and sends `ExecutionReport` to `codehive_result_queue` via `ExecutionResultProducer` + - Backend receives results via `ExecutionResultListener` and updates `Execution` entity via `ExecutionResultService` 2. **MinIO Integration**: ✅ **IMPLEMENTED** - Backend and worker use MinIO for object storage. Source code and test inputs are stored as files in MinIO bucket (`codehive`), referenced by object keys in `ExecutionJob`. @@ -1202,6 +1301,12 @@ void login_WithWrongPassword_ThrowsIncorrectCredentialsException() { - Comprehensive execution reports with statistics (passed/failed, timing, memory) - JSON report upload to MinIO for backend consumption +8. **Result Publishing Pipeline**: ✅ **IMPLEMENTED** - Complete result flow from worker to backend: + - Worker's `ExecutionResultProducer` publishes `ExecutionReport` to `codehive_result_queue` + - Backend's `ExecutionResultListener` consumes results from the queue + - `ExecutionResultService` updates `Execution` entity with status, timing, and memory stats + - Handles error cases by sending error reports back to backend + --- ## Troubleshooting @@ -1279,7 +1384,7 @@ void login_WithWrongPassword_ThrowsIncorrectCredentialsException() { ## Future Enhancements -1. **Result Publishing**: ✅ **NEXT** - Send `ExecutionReport` back to backend via RabbitMQ result queue +1. ~~**Result Publishing**~~: ✅ **COMPLETED** - Bidirectional RabbitMQ communication implemented 2. **Real-time Collaboration**: WebSocket support for live code editing 3. **Group Management**: Teacher/student group creation and assignment submission 4. **File Upload**: Support for uploading code files and project archives @@ -1297,10 +1402,10 @@ void login_WithWrongPassword_ThrowsIncorrectCredentialsException() { --- -**Last Updated**: 2026-01-21 +**Last Updated**: 2026-01-22 **Current Branch**: `feature/rabbitMQ` **Project Status**: Active Development (Alpha) -**Worker Status**: ✅ **FULLY OPERATIONAL** - Complete test execution pipeline implemented: +**Worker Status**: ✅ **FULLY OPERATIONAL** - Complete bidirectional communication pipeline: - RabbitMQ integration with comprehensive `ExecutionJob` model - MinIO integration for source code, test cases, and execution reports - All 4 language executors (Java, Python, C, C++) with Docker sandboxing @@ -1310,4 +1415,5 @@ void login_WithWrongPassword_ThrowsIncorrectCredentialsException() { - Comprehensive execution reports with per-test-case results and statistics - TLE/MLE/RTE/CE/AC/WA verdict detection - Report storage in MinIO as JSON -- Next: Result publishing to backend via RabbitMQ result queue +- ✅ Result publishing to backend via `codehive_result_queue` +- ✅ Backend listener updates `Execution` entity with results diff --git a/codehive-backend/src/main/java/com/github/codehive/config/MinioConfig.java b/codehive-backend/src/main/java/com/github/codehive/config/MinioConfig.java index 4264858..c4b90f8 100644 --- a/codehive-backend/src/main/java/com/github/codehive/config/MinioConfig.java +++ b/codehive-backend/src/main/java/com/github/codehive/config/MinioConfig.java @@ -1,5 +1,6 @@ package com.github.codehive.config; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -7,13 +8,22 @@ @Configuration public class MinioConfig { + @Value("${minio.url}") + private String minioUrl; + + @Value("${minio.accessKey}") + private String minioAccessKey; + + @Value("${minio.secretKey}") + private String minioSecretKey; + @Bean MinioClient minioClient() { return MinioClient.builder() - .endpoint(System.getProperty("minio.url", "http://exampleurl.com:9000")) + .endpoint(minioUrl) .credentials( - System.getProperty("minio.accessKey", "accesskey"), - System.getProperty("minio.secretKey", "secretkey")) + minioAccessKey, + minioSecretKey) .build(); } } diff --git a/codehive-backend/src/main/java/com/github/codehive/config/RabbitConfig.java b/codehive-backend/src/main/java/com/github/codehive/config/RabbitConfig.java index 48723c1..226e0b4 100644 --- a/codehive-backend/src/main/java/com/github/codehive/config/RabbitConfig.java +++ b/codehive-backend/src/main/java/com/github/codehive/config/RabbitConfig.java @@ -1,16 +1,28 @@ package com.github.codehive.config; import org.springframework.amqp.core.Queue; - +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RabbitConfig { public static final String QUEUE_NAME = System.getProperty("rabbitmq.queue", "codehive_queue"); + public static final String RESULT_QUEUE_NAME = System.getProperty("rabbitmq.result.queue", "codehive_result_queue"); @Bean - Queue executionQueue(){ + Queue executionQueue() { return new Queue(QUEUE_NAME, true); } + + @Bean + Queue resultQueue() { + return new Queue(RESULT_QUEUE_NAME, true); + } + + @Bean + public MessageConverter jsonMessageConverter() { + return new Jackson2JsonMessageConverter(); + } } diff --git a/codehive-backend/src/main/java/com/github/codehive/config/SecurityConfig.java b/codehive-backend/src/main/java/com/github/codehive/config/SecurityConfig.java index 6d8dd15..a251d45 100644 --- a/codehive-backend/src/main/java/com/github/codehive/config/SecurityConfig.java +++ b/codehive-backend/src/main/java/com/github/codehive/config/SecurityConfig.java @@ -50,8 +50,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti // Authentication endpoints .requestMatchers(HttpMethod.POST, "/api/auth/login").permitAll() // Password recovery endpoints - .requestMatchers(HttpMethod.POST, "/api/recovery-password/**").permitAll() - // WebSocket endpoint + .requestMatchers(HttpMethod.POST, "/api/recovery-password/**", "/api/execution/**").permitAll() // WebSocket endpoint .requestMatchers("/ws/**").permitAll() // Swagger/OpenAPI documentation .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() diff --git a/codehive-backend/src/main/java/com/github/codehive/controller/CheckExecutionController.java b/codehive-backend/src/main/java/com/github/codehive/controller/CheckExecutionController.java new file mode 100644 index 0000000..4f76924 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/controller/CheckExecutionController.java @@ -0,0 +1,117 @@ +package com.github.codehive.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.github.codehive.model.dto.ExecutionDTO; +import com.github.codehive.model.request.execution.ExecutionRequest; +import com.github.codehive.model.response.ErrorResponse; +import com.github.codehive.model.response.SuccessResponse; +import com.github.codehive.ratelimit.RateLimit; +import com.github.codehive.service.ExecutionRequestService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Controller for code execution endpoints. + * Allows submitting code for execution and checking execution status. + */ +@RestController +@RequestMapping("/api/execution") +@Tag(name = "Code Execution", description = "Code execution and result checking APIs") +public class CheckExecutionController { + private static final Logger logger = LoggerFactory.getLogger(CheckExecutionController.class); + + private final ExecutionRequestService executionRequestService; + + public CheckExecutionController(ExecutionRequestService executionRequestService) { + this.executionRequestService = executionRequestService; + } + + @Operation( + summary = "Submit code for execution", + description = "Submits code to be executed in a sandboxed environment. Returns execution ID for status polling." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "202", + description = "Execution request accepted", + content = @Content(schema = @Schema(implementation = SuccessResponse.class)) + ), + @ApiResponse( + responseCode = "400", + description = "Validation error", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ), + @ApiResponse( + responseCode = "429", + description = "Too many requests", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ) + }) + @RateLimit(limit = 10, duration = 60, message = "Too many execution requests. Please try again in 1 minute.") + @PostMapping("/check") + public ResponseEntity> submitExecution( + @Valid @RequestBody ExecutionRequest request) { + logger.info("[WORKFLOW] POST /api/execution/check - Received execution request: language={}, executionType={}, codeLength={}", + request.getLanguage(), request.getExecutionType(), + request.getCode() != null ? request.getCode().length() : 0); + + ExecutionDTO execution = executionRequestService.requestExecution(request); + + logger.info("[WORKFLOW] POST /api/execution/check - Execution created: executionId={}, status={}", + execution.getId(), execution.getStatus()); + + SuccessResponse response = new SuccessResponse<>( + "Execution request submitted successfully", execution); + return ResponseEntity.status(HttpStatus.ACCEPTED).body(response); + } + + @Operation( + summary = "Get execution status", + description = "Retrieves the current status and results of a code execution by its ID." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Execution found", + content = @Content(schema = @Schema(implementation = SuccessResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = "Execution not found", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ) + }) + @GetMapping("/check/{id}") + public ResponseEntity> getExecution( + @Parameter(description = "Execution ID", required = true) + @PathVariable Long id) { + logger.info("[WORKFLOW] GET /api/execution/check/{} - Fetching execution status", id); + + ExecutionDTO execution = executionRequestService.getExecutionById(id); + + logger.info("[WORKFLOW] GET /api/execution/check/{} - Execution found: status={}, timeMs={}, memoryMb={}", + id, execution.getStatus(), execution.getTimeMs(), execution.getMemoryMb()); + + SuccessResponse response = new SuccessResponse<>( + "Execution retrieved successfully", execution); + return ResponseEntity.ok(response); + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/messaging/listener/ExecutionResultListener.java b/codehive-backend/src/main/java/com/github/codehive/messaging/listener/ExecutionResultListener.java new file mode 100644 index 0000000..29384e0 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/messaging/listener/ExecutionResultListener.java @@ -0,0 +1,37 @@ +package com.github.codehive.messaging.listener; + +import com.github.codehive.model.dto.queue.ExecutionReport; +import com.github.codehive.service.ExecutionResultService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +@Component +public class ExecutionResultListener { + private static final Logger logger = LoggerFactory.getLogger(ExecutionResultListener.class); + + private final ExecutionResultService executionResultService; + + public ExecutionResultListener(ExecutionResultService executionResultService) { + this.executionResultService = executionResultService; + } + + @RabbitListener(queues = "${rabbitmq.result.queue:codehive_result_queue}") + public void handleExecutionResult(ExecutionReport report) { + logger.info("[WORKFLOW] RABBITMQ RECEIVE: Received execution result from worker - executionId={}, overallStatus={}", + report.getExecutionId(), report.getOverallStatus()); + logger.info("[WORKFLOW] RABBITMQ RECEIVE: Result details - passed={}/{}, maxTimeMs={}, maxMemoryKb={}", + report.getPassedTests(), report.getTotalTests(), + report.getMaxExecutionTimeMs(), report.getMaxMemoryUsedKb()); + + try { + executionResultService.processExecutionResult(report); + logger.info("[WORKFLOW] RABBITMQ RECEIVE: Successfully processed result - executionId={}", + report.getExecutionId()); + } catch (Exception e) { + logger.error("[WORKFLOW] RABBITMQ RECEIVE FAILED: Could not process result - executionId={}, error={}", + report.getExecutionId(), e.getMessage(), e); + } + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/messaging/producer/ExecutionRequestProducer.java b/codehive-backend/src/main/java/com/github/codehive/messaging/producer/ExecutionRequestProducer.java new file mode 100644 index 0000000..12a3cd0 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/messaging/producer/ExecutionRequestProducer.java @@ -0,0 +1,37 @@ +package com.github.codehive.messaging.producer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.stereotype.Service; + +import com.github.codehive.config.RabbitConfig; +import com.github.codehive.model.dto.queue.ExecutionJob; + +@Service +public class ExecutionRequestProducer { + private static final Logger logger = LoggerFactory.getLogger(ExecutionRequestProducer.class); + + private final RabbitTemplate rabbitTemplate; + + public ExecutionRequestProducer(RabbitTemplate rabbitTemplate) { + this.rabbitTemplate = rabbitTemplate; + } + + public void sendExecutionRequest(ExecutionJob job) { + logger.info("[WORKFLOW] RABBITMQ SEND: Preparing to send execution request - executionId={}, language={}, executionType={}", + job.getId(), job.getLanguage(), job.getExecutionType()); + logger.info("[WORKFLOW] RABBITMQ SEND: Job details - sourceKey={}, timeLimitMs={}, memoryLimitMb={}, numTests={}", + job.getSource(), job.getTimeLimitMs(), job.getMemoryLimitMb(), job.getNumTests()); + + try { + rabbitTemplate.convertAndSend(RabbitConfig.QUEUE_NAME, job); + logger.info("[WORKFLOW] RABBITMQ SEND: Successfully sent to queue '{}' - executionId={}", + RabbitConfig.QUEUE_NAME, job.getId()); + } catch (Exception e) { + logger.error("[WORKFLOW] RABBITMQ SEND FAILED: Could not send to queue '{}' - executionId={}, error={}", + RabbitConfig.QUEUE_NAME, job.getId(), e.getMessage(), e); + throw e; + } + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/ExecutionDTO.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/ExecutionDTO.java index adec652..c195571 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/dto/ExecutionDTO.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/ExecutionDTO.java @@ -10,6 +10,7 @@ public class ExecutionDTO { private Long id; private Long submissionId; + private Long userId; private ExecutionType executionType; private ExecutionStatus status; private Long timeMs; @@ -33,6 +34,14 @@ public void setSubmissionId(Long submissionId) { this.submissionId = submissionId; } + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + public ExecutionType getExecutionType() { return executionType; } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/ExecutionReport.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/ExecutionReport.java new file mode 100644 index 0000000..db4401f --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/ExecutionReport.java @@ -0,0 +1,108 @@ +package com.github.codehive.model.dto.queue; + +import com.github.codehive.model.enums.ExecutionStatus; + +import java.util.ArrayList; +import java.util.List; + +public class ExecutionReport { + private Long executionId; + private ExecutionStatus overallStatus; + private List testCaseResults; + private int totalTests; + private int passedTests; + private int failedTests; + private Long totalExecutionTimeMs; + private Long maxExecutionTimeMs; + private Long maxMemoryUsedKb; + private String compilationError; + + public ExecutionReport() { + this.testCaseResults = new ArrayList<>(); + } + + public ExecutionReport(Long executionId) { + this.executionId = executionId; + this.testCaseResults = new ArrayList<>(); + } + + public Long getExecutionId() { + return executionId; + } + + public void setExecutionId(Long executionId) { + this.executionId = executionId; + } + + public ExecutionStatus getOverallStatus() { + return overallStatus; + } + + public void setOverallStatus(ExecutionStatus overallStatus) { + this.overallStatus = overallStatus; + } + + public List getTestCaseResults() { + return testCaseResults; + } + + public void setTestCaseResults(List testCaseResults) { + this.testCaseResults = testCaseResults; + } + + public int getTotalTests() { + return totalTests; + } + + public void setTotalTests(int totalTests) { + this.totalTests = totalTests; + } + + public int getPassedTests() { + return passedTests; + } + + public void setPassedTests(int passedTests) { + this.passedTests = passedTests; + } + + public int getFailedTests() { + return failedTests; + } + + public void setFailedTests(int failedTests) { + this.failedTests = failedTests; + } + + public Long getTotalExecutionTimeMs() { + return totalExecutionTimeMs; + } + + public void setTotalExecutionTimeMs(Long totalExecutionTimeMs) { + this.totalExecutionTimeMs = totalExecutionTimeMs; + } + + public Long getMaxExecutionTimeMs() { + return maxExecutionTimeMs; + } + + public void setMaxExecutionTimeMs(Long maxExecutionTimeMs) { + this.maxExecutionTimeMs = maxExecutionTimeMs; + } + + public Long getMaxMemoryUsedKb() { + return maxMemoryUsedKb; + } + + public void setMaxMemoryUsedKb(Long maxMemoryUsedKb) { + this.maxMemoryUsedKb = maxMemoryUsedKb; + } + + public String getCompilationError() { + return compilationError; + } + + public void setCompilationError(String compilationError) { + this.compilationError = compilationError; + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/TestCaseResult.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/TestCaseResult.java new file mode 100644 index 0000000..88b9c06 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/TestCaseResult.java @@ -0,0 +1,62 @@ +package com.github.codehive.model.dto.queue; + +import com.github.codehive.model.enums.ExecutionStatus; + +public class TestCaseResult { + private int testCaseNumber; + private ExecutionStatus status; + private Long executionTimeMs; + private Long memoryUsedKb; + private String feedback; + + public TestCaseResult() { + } + + public TestCaseResult(int testCaseNumber, ExecutionStatus status, + Long executionTimeMs, Long memoryUsedKb) { + this.testCaseNumber = testCaseNumber; + this.status = status; + this.executionTimeMs = executionTimeMs; + this.memoryUsedKb = memoryUsedKb; + } + + public int getTestCaseNumber() { + return testCaseNumber; + } + + public void setTestCaseNumber(int testCaseNumber) { + this.testCaseNumber = testCaseNumber; + } + + public ExecutionStatus getStatus() { + return status; + } + + public void setStatus(ExecutionStatus status) { + this.status = status; + } + + public Long getExecutionTimeMs() { + return executionTimeMs; + } + + public void setExecutionTimeMs(Long executionTimeMs) { + this.executionTimeMs = executionTimeMs; + } + + public Long getMemoryUsedKb() { + return memoryUsedKb; + } + + public void setMemoryUsedKb(Long memoryUsedKb) { + this.memoryUsedKb = memoryUsedKb; + } + + public String getFeedback() { + return feedback; + } + + public void setFeedback(String feedback) { + this.feedback = feedback; + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/entity/Execution.java b/codehive-backend/src/main/java/com/github/codehive/model/entity/Execution.java index f80f995..b1cbda8 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/entity/Execution.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/entity/Execution.java @@ -15,6 +15,7 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; import jakarta.persistence.Table; @Entity @@ -24,10 +25,14 @@ public class Execution { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "submission_id", nullable = true) + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "submission_id", nullable = true, unique = true) private Submission submission; // NULLABLE for PRACTICE executions + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = true) + private User user; // User who initiated the execution + @Column(nullable = false, length = 20) @Enumerated(EnumType.STRING) private ExecutionType executionType; @@ -65,6 +70,12 @@ public Execution(ExecutionType executionType) { this.executionType = executionType; } + public Execution(ExecutionType executionType, User user) { + this(); + this.executionType = executionType; + this.user = user; + } + public Long getId() { return id; } @@ -81,6 +92,14 @@ public void setSubmission(Submission submission) { this.submission = submission; } + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + public ExecutionType getExecutionType() { return executionType; } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/mapper/ExecutionMapper.java b/codehive-backend/src/main/java/com/github/codehive/model/mapper/ExecutionMapper.java index a537332..154b6fe 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/mapper/ExecutionMapper.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/mapper/ExecutionMapper.java @@ -14,6 +14,8 @@ public static ExecutionDTO toDTO(Execution execution) { dto.setId(execution.getId()); dto.setSubmissionId(execution.getSubmission() != null ? execution.getSubmission().getId() : null); + dto.setUserId(execution.getUser() != null ? + execution.getUser().getId() : null); dto.setExecutionType(execution.getExecutionType()); dto.setStatus(execution.getStatus()); dto.setTimeMs(execution.getTimeMs()); @@ -29,7 +31,7 @@ public static Execution toEntity(ExecutionDTO dto) { } Execution execution = new Execution(); execution.setId(dto.getId()); - // Note: Submission must be set separately via submission repository + // Note: Submission and User must be set separately via their repositories execution.setExecutionType(dto.getExecutionType()); execution.setStatus(dto.getStatus()); execution.setTimeMs(dto.getTimeMs()); diff --git a/codehive-backend/src/main/java/com/github/codehive/model/request/execution/ExecutionRequest.java b/codehive-backend/src/main/java/com/github/codehive/model/request/execution/ExecutionRequest.java new file mode 100644 index 0000000..2044afd --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/request/execution/ExecutionRequest.java @@ -0,0 +1,87 @@ +package com.github.codehive.model.request.execution; + +import java.util.List; + +import com.github.codehive.model.enums.ExecutionType; +import com.github.codehive.model.enums.Language; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public class ExecutionRequest { + @NotBlank(message = "Code is required") + private String code; + + @NotNull(message = "Language is required") + private Language language; + + private Long requesterId; + + private Long assignmentId; + + private List testCases; + + @NotNull(message = "Execution type is required") + private ExecutionType executionType; + + public ExecutionRequest() { + } + + public ExecutionRequest(String code, Language language, Long requesterId, + Long assignmentId, List testCases, ExecutionType executionType) { + this.code = code; + this.language = language; + this.requesterId = requesterId; + this.assignmentId = assignmentId; + this.testCases = testCases; + this.executionType = executionType; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public Language getLanguage() { + return language; + } + + public void setLanguage(Language language) { + this.language = language; + } + + public Long getRequesterId() { + return requesterId; + } + + public void setRequesterId(Long requesterId) { + this.requesterId = requesterId; + } + + public Long getAssignmentId() { + return assignmentId; + } + + public void setAssignmentId(Long assignmentId) { + this.assignmentId = assignmentId; + } + + public List getTestCases() { + return testCases; + } + + public void setTestCases(List testCases) { + this.testCases = testCases; + } + + public ExecutionType getExecutionType() { + return executionType; + } + + public void setExecutionType(ExecutionType executionType) { + this.executionType = executionType; + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/service/ExecutionProducer.java b/codehive-backend/src/main/java/com/github/codehive/service/ExecutionProducer.java deleted file mode 100644 index 019eba2..0000000 --- a/codehive-backend/src/main/java/com/github/codehive/service/ExecutionProducer.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.github.codehive.service; - -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.stereotype.Service; - -import com.github.codehive.config.RabbitConfig; -import com.github.codehive.model.dto.queue.ExecutionJob; - -@Service -public class ExecutionProducer { - private final RabbitTemplate rabbitTemplate; - - public ExecutionProducer(RabbitTemplate rabbitTemplate) { - this.rabbitTemplate = rabbitTemplate; - } - - public void sendExecutionRequest(ExecutionJob executionJob) { - rabbitTemplate.convertAndSend( - RabbitConfig.QUEUE_NAME, - executionJob - ); - } -} diff --git a/codehive-backend/src/main/java/com/github/codehive/service/ExecutionRequestService.java b/codehive-backend/src/main/java/com/github/codehive/service/ExecutionRequestService.java new file mode 100644 index 0000000..f7404a1 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/service/ExecutionRequestService.java @@ -0,0 +1,140 @@ +package com.github.codehive.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.github.codehive.messaging.producer.ExecutionRequestProducer; +import com.github.codehive.model.dto.ExecutionDTO; +import com.github.codehive.model.dto.queue.ExecutionJob; +import com.github.codehive.model.entity.Execution; +import com.github.codehive.model.entity.User; +import com.github.codehive.model.enums.ComparatorType; +import com.github.codehive.model.enums.ExecutionType; +import com.github.codehive.model.enums.Language; +import com.github.codehive.model.exception.EntityNotFoundException; +import com.github.codehive.model.mapper.ExecutionMapper; +import com.github.codehive.model.request.execution.ExecutionRequest; +import com.github.codehive.repository.ExecutionRepository; +import com.github.codehive.repository.UserRepository; +import com.github.codehive.utils.FileExtensionUtil; +import com.github.codehive.utils.ObjectKeyBuilder; + +/** + * + * TODO: Fix this class for a general execution request service. Currently, it is only for manual test execution. + * + */ + +@Service +public class ExecutionRequestService { + private static final Logger logger = LoggerFactory.getLogger(ExecutionRequestService.class); + + private static final Long DEFAULT_TIME_LIMIT_MS = 5000L; + private static final Long DEFAULT_MEMORY_LIMIT_MB = 256L; + + private final ExecutionRequestProducer executionRequestProducer; + private final ExecutionRepository executionRepository; + private final ObjectStorageService objectStorageService; + private final UserRepository userRepository; + + public ExecutionRequestService(ExecutionRequestProducer executionRequestProducer, + ExecutionRepository executionRepository, + ObjectStorageService objectStorageService, + UserRepository userRepository) { + this.executionRequestProducer = executionRequestProducer; + this.executionRepository = executionRepository; + this.objectStorageService = objectStorageService; + this.userRepository = userRepository; + } + + @Transactional + public ExecutionDTO requestExecution(ExecutionRequest request) { + logger.info("[WORKFLOW] Step 1: Creating execution entity - language={}, executionType={}, assignmentId={}", + request.getLanguage(), request.getExecutionType(), request.getAssignmentId()); + + Execution execution = new Execution(request.getExecutionType()); + + if (request.getRequesterId() != null) { + User user = userRepository.findById(request.getRequesterId()) + .orElseThrow(() -> new EntityNotFoundException("User not found with id: " + request.getRequesterId())); + execution.setUser(user); + } + + execution = executionRepository.save(execution); + logger.info("[WORKFLOW] Step 2: Execution entity saved to database - executionId={}, status={}", + execution.getId(), execution.getStatus()); + + String extension = FileExtensionUtil.getFileExtensionByLanguage(request.getLanguage()); + String codeStorageKey = ObjectKeyBuilder.executionSourceCode(execution.getId(), extension); + + logger.info("[WORKFLOW] Step 3: Uploading source code to MinIO - key={}, codeLength={}", + codeStorageKey, request.getCode() != null ? request.getCode().length() : 0); + + try { + objectStorageService.upload(codeStorageKey, request.getCode()); + logger.info("[WORKFLOW] Step 3: Source code uploaded successfully - key={}", codeStorageKey); + } catch (Exception e) { + logger.error("[WORKFLOW] Step 3 FAILED: Could not upload source code - executionId={}, error={}", + execution.getId(), e.getMessage(), e); + throw new RuntimeException("Failed to upload source code to object storage", e); + } + + ExecutionJob job = buildExecutionJob(execution, request, extension); + logger.info("[WORKFLOW] Step 4: Built ExecutionJob - executionId={}, sourceKey={}, outputKey={}, timeLimitMs={}, memoryLimitMb={}", + job.getId(), job.getSource(), job.getOutputPath(), job.getTimeLimitMs(), job.getMemoryLimitMb()); + + logger.info("[WORKFLOW] Step 5: Sending execution job to RabbitMQ queue"); + executionRequestProducer.sendExecutionRequest(job); + + logger.info("[WORKFLOW] Execution request workflow completed - executionId={}", execution.getId()); + return ExecutionMapper.toDTO(execution); + } + + @Transactional(readOnly = true) + public ExecutionDTO getExecutionById(Long id) { + Execution execution = executionRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("Execution not found with id: " + id)); + return ExecutionMapper.toDTO(execution); + } + + private ExecutionJob buildExecutionJob(Execution execution, ExecutionRequest request, String extension) { + String sourceKey = ObjectKeyBuilder.executionSourceCode(execution.getId(), extension); + String outputKey = ObjectKeyBuilder.executionTestCaseOutput(execution.getId()); + + if (request.getExecutionType() == ExecutionType.PRACTICE) { + return new ExecutionJob( + execution.getId(), + sourceKey, + ObjectKeyBuilder.referenceSolutionSourceCode(request.getAssignmentId(), "java"), // This will depend on assignment settings in future + request.getLanguage(), + request.getExecutionType(), + request.getTestCases(), + DEFAULT_TIME_LIMIT_MS, // This will depend on assignment settings in future + DEFAULT_MEMORY_LIMIT_MB, // This will depend on assignment settings in future + ComparatorType.EXACT_MATCH, // This will depend on assignment settings in future + outputKey, + request.getTestCases() != null ? request.getTestCases().size() : 0, + ObjectKeyBuilder.testsPath(request.getAssignmentId()), + Language.JAVA // This will deépend on assignment settings in future + ); + } else { + return new ExecutionJob( + execution.getId(), + sourceKey, + null, + request.getLanguage(), + request.getExecutionType(), + null, + DEFAULT_TIME_LIMIT_MS, // This will depend on assignment settings in future + DEFAULT_MEMORY_LIMIT_MB, // This will depend on assignment settings in future + ComparatorType.EXACT_MATCH, // This will depend on assignment settings in future + outputKey, + 5, // This will depend on assignment settings in future + ObjectKeyBuilder.testsPath(request.getAssignmentId()), + Language.JAVA // This will depend on assignment settings in future + ); + } + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/service/ExecutionResultService.java b/codehive-backend/src/main/java/com/github/codehive/service/ExecutionResultService.java new file mode 100644 index 0000000..ed0ebe0 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/service/ExecutionResultService.java @@ -0,0 +1,61 @@ +package com.github.codehive.service; + +import com.github.codehive.model.dto.queue.ExecutionReport; +import com.github.codehive.model.entity.Execution; +import com.github.codehive.repository.ExecutionRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class ExecutionResultService { + private static final Logger logger = LoggerFactory.getLogger(ExecutionResultService.class); + + private final ExecutionRepository executionRepository; + + public ExecutionResultService(ExecutionRepository executionRepository) { + this.executionRepository = executionRepository; + } + + @Transactional + public void processExecutionResult(ExecutionReport report) { + logger.info("[WORKFLOW] DB UPDATE: Processing execution result - executionId={}, status={}", + report.getExecutionId(), report.getOverallStatus()); + + Execution execution = executionRepository.findById(report.getExecutionId()) + .orElse(null); + + if (execution == null) { + logger.warn("[WORKFLOW] DB UPDATE SKIPPED: Execution not found in database - executionId={}", + report.getExecutionId()); + return; + } + + logger.info("[WORKFLOW] DB UPDATE: Found execution in database - executionId={}, currentStatus={}", + execution.getId(), execution.getStatus()); + + // Update execution status + execution.setStatus(report.getOverallStatus()); + + // Update timing (use max execution time as representative) + if (report.getMaxExecutionTimeMs() != null) { + execution.setTimeMs(report.getMaxExecutionTimeMs()); + } + + // Update memory (convert KB to MB, use max memory) + if (report.getMaxMemoryUsedKb() != null) { + execution.setMemoryMb(report.getMaxMemoryUsedKb() / 1024); + } + + executionRepository.save(execution); + + logger.info("[WORKFLOW] DB UPDATE: Execution updated successfully - executionId={}, newStatus={}, timeMs={}, memoryMb={}, passed={}/{}", + execution.getId(), + execution.getStatus(), + execution.getTimeMs(), + execution.getMemoryMb(), + report.getPassedTests(), + report.getTotalTests()); + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/service/ObjectStorageService.java b/codehive-backend/src/main/java/com/github/codehive/service/ObjectStorageService.java index 9120088..b6fda14 100644 --- a/codehive-backend/src/main/java/com/github/codehive/service/ObjectStorageService.java +++ b/codehive-backend/src/main/java/com/github/codehive/service/ObjectStorageService.java @@ -1,15 +1,21 @@ package com.github.codehive.service; +import java.io.ByteArrayInputStream; import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import io.minio.MinioClient; +import io.minio.PutObjectArgs; @Service public class ObjectStorageService { private final MinioClient minioClient; - private final String bucketName = System.getProperty("minio.bucketName", "codehive"); + + @Value("${minio.bucketName}") + private String bucketName; public ObjectStorageService(MinioClient minioClient) { this.minioClient = minioClient; @@ -26,6 +32,21 @@ public void upload(String objectKey, InputStream data, long size, String content ); } + public void upload(String objectKey, String content) throws Exception { + byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8); + ByteArrayInputStream stream = new ByteArrayInputStream(contentBytes); + + minioClient.putObject( + PutObjectArgs.builder() + .bucket(bucketName) + .object(objectKey) + .stream(stream, contentBytes.length, -1) + .contentType("text/plain") + .build() + ); + } + + public InputStream download(String objectKey) throws Exception { return minioClient.getObject( io.minio.GetObjectArgs.builder() diff --git a/codehive-backend/src/main/java/com/github/codehive/utils/FileExtensionUtil.java b/codehive-backend/src/main/java/com/github/codehive/utils/FileExtensionUtil.java new file mode 100644 index 0000000..dc61b4e --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/utils/FileExtensionUtil.java @@ -0,0 +1,20 @@ +package com.github.codehive.utils; + +import com.github.codehive.model.enums.Language; + +public class FileExtensionUtil { + public static String getFileExtensionByLanguage(Language language) { + switch (language) { + case JAVA: + return "java"; + case PYTHON: + return "py"; + case CPP: + return "cpp"; + case C: + return "c"; + default: + throw new IllegalArgumentException("Unsupported language: " + language); + } + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java b/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java index 6212707..ce63e2a 100644 --- a/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java +++ b/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java @@ -9,6 +9,11 @@ public static String testCaseOutput(Long assignmentId, Long testCaseId) { return String.format("test-suites/assignments/%d/tc-%d/tc%d.out", assignmentId, testCaseId, testCaseId); } + public static String testsPath(Long assignmentId) { + return String.format("test-suites/assignments/%d/", assignmentId); + + } + public static String submissionSourceCode(Long assignmentId, Long submissionId, String fileExtension, Long groupId) { return String.format("submissions/groups/%d/assignments/%d/submission-%d/Main.%s", groupId, assignmentId, submissionId, fileExtension); } @@ -17,11 +22,15 @@ public static String executionOutput(Long submissionId, Long executionId, Long g return String.format("executions/groups/%d/assignments/%d/execution-%d/output.%s", groupId, submissionId, executionId, fileExtension); } - public static String referenceSolutionSourceCode(Long assignmentId, Long referenceSolutionId, String fileExtension) { - return String.format("reference-solutions/assignments/%d/reference-solution-%d/Main.%s", assignmentId, referenceSolutionId, fileExtension); + public static String referenceSolutionSourceCode(Long assignmentId, String fileExtension) { + return String.format("test-suites/assignments/%d/reference/Main.%s", assignmentId, fileExtension); + } + + public static String executionTestCaseOutput(Long executionId) { + return String.format("test-execution/execution-%d/output/", executionId); } - public static String executionTestCaseOutput(Long executionId, String fileExtension) { - return String.format("test-execution/execution-%d/output.%s", executionId, fileExtension); + public static String executionSourceCode(Long executionId, String fileExtension) { + return String.format("test-execution/execution-%d/source.%s", executionId, fileExtension); } } diff --git a/codehive-backend/src/main/resources/application.properties b/codehive-backend/src/main/resources/application.properties index 21d4ba6..42e36f7 100644 --- a/codehive-backend/src/main/resources/application.properties +++ b/codehive-backend/src/main/resources/application.properties @@ -2,9 +2,9 @@ spring.config.import=optional:file:.env[.properties] spring.application.name=codehive -spring.datasource.url=${DATABASE_URL} -spring.datasource.username=${DATABASE_USERNAME} -spring.datasource.password=${DATABASE_PASSWORD} +spring.datasource.url=${DATABASE_URL:dburlexample} +spring.datasource.username=${DATABASE_USERNAME:default} +spring.datasource.password=${DATABASE_PASSWORD:default} spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=update @@ -16,30 +16,30 @@ server.port=8080 # JWT Configuration # The secret must be a Base64-encoded key of at least 256 bits (32 bytes) for HS256 -security.jwt.secret=${JWT_SECRET} -security.jwt.expiration=${JWT_EXPIRATION} +security.jwt.secret=${JWT_SECRET:defaultSecret} +security.jwt.expiration=${JWT_EXPIRATION:86400000} # Java Mail Configuration -spring.mail.host=${MAIL_HOST} -spring.mail.port=${MAIL_PORT} -spring.mail.username=${MAIL_USERNAME} -spring.mail.password=${MAIL_PASSWORD} -spring.mail.properties.mail.smtp.auth=${MAIL_SMTP_AUTH} -spring.mail.properties.mail.smtp.starttls.enable=${MAIL_STARTTLS_ENABLE} +spring.mail.host=${MAIL_HOST:defaultHost} +spring.mail.port=${MAIL_PORT:587} +spring.mail.username=${MAIL_USERNAME:defaultUser} +spring.mail.password=${MAIL_PASSWORD:defaultPassword} +spring.mail.properties.mail.smtp.auth=${MAIL_SMTP_AUTH:true} +spring.mail.properties.mail.smtp.starttls.enable=${MAIL_STARTTLS_ENABLE:true} # Frontend URL -frontend.url=${FRONTEND_URL} +frontend.url=${FRONTEND_URL:defaultFrontendUrl} # Minio Configuration -minio.url=${MINIO_URL} -minio.accessKey=${MINIO_ACCESS_KEY} -minio.secretKey=${MINIO_SECRET_KEY} -minio.bucketName=${MINIO_BUCKET_NAME} +minio.url=${MINIO_URL:defaultMinioUrl} +minio.accessKey=${MINIO_ACCESS_KEY:defaultAccessKey} +minio.secretKey=${MINIO_SECRET_KEY:defaultSecretKey} +minio.bucketName=${MINIO_BUCKET_NAME:defaultBucketName} # RabbitMQ Configuration -spring.rabbitmq.host=${RABBITMQ_HOST} -spring.rabbitmq.port=${RABBITMQ_PORT} -spring.rabbitmq.username=${RABBITMQ_USERNAME} -spring.rabbitmq.password=${RABBITMQ_PASSWORD} -spring.rabbitmq.queue=${RABBITMQ_QUEUE} - +spring.rabbitmq.host=${RABBITMQ_HOST:defaultRabbitHost} +spring.rabbitmq.port=${RABBITMQ_PORT:5672} +spring.rabbitmq.username=${RABBITMQ_USERNAME:defaultRabbitUser} +spring.rabbitmq.password=${RABBITMQ_PASSWORD:defaultRabbitPassword} +spring.rabbitmq.queue=${RABBITMQ_QUEUE:defaultQueue} +spring.rabbitmq.result.queue=${RABBITMQ_RESULT_QUEUE:defaultResultQueue} \ No newline at end of file diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/config/MinioConfig.java b/codehive-worker/src/main/java/com/github/codehive/worker/config/MinioConfig.java index 05b13a6..cdbcca5 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/config/MinioConfig.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/config/MinioConfig.java @@ -1,18 +1,28 @@ package com.github.codehive.worker.config; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import io.minio.MinioClient; @Configuration public class MinioConfig { + @Value("${minio.url}") + private String minioUrl; + + @Value("${minio.accessKey}") + private String minioAccessKey; + + @Value("${minio.secretKey}") + private String minioSecretKey; + @Bean MinioClient minioClient() { return MinioClient.builder() - .endpoint(System.getProperty("minio.url", "http://localhost:9000")) + .endpoint(minioUrl) .credentials( - System.getProperty("minio.accessKey", "minioadmin"), - System.getProperty("minio.secretKey", "minioadmin")) + minioAccessKey, + minioSecretKey) .build(); } } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/config/RabbitMQConfig.java b/codehive-worker/src/main/java/com/github/codehive/worker/config/RabbitMQConfig.java index bb4a2a0..a7b6cd8 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/config/RabbitMQConfig.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/config/RabbitMQConfig.java @@ -9,12 +9,18 @@ @Configuration public class RabbitMQConfig { public static final String QUEUE_NAME = System.getProperty("rabbitmq.queue", "codehive_queue"); + public static final String RESULT_QUEUE_NAME = System.getProperty("rabbitmq.result.queue", "codehive_result_queue"); @Bean Queue executionQueue() { return new Queue(QUEUE_NAME, true); } + @Bean + Queue resultQueue() { + return new Queue(RESULT_QUEUE_NAME, true); + } + @Bean @SuppressWarnings("removal") public MessageConverter jsonMessageConverter() { diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/SubmissionListener.java b/codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/SubmissionListener.java index f553a1d..f1e20a2 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/SubmissionListener.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/SubmissionListener.java @@ -1,7 +1,9 @@ package com.github.codehive.worker.messaging.listener; +import com.github.codehive.worker.messaging.producer.ExecutionResultProducer; import com.github.codehive.worker.model.dto.ExecutionReport; import com.github.codehive.worker.model.dto.queue.ExecutionJob; +import com.github.codehive.worker.model.enums.ExecutionStatus; import com.github.codehive.worker.service.TestExecutionService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,15 +15,32 @@ public class SubmissionListener { private static final Logger logger = LoggerFactory.getLogger(SubmissionListener.class); private final TestExecutionService testExecutionService; + private final ExecutionResultProducer executionResultProducer; - public SubmissionListener(TestExecutionService testExecutionService) { + public SubmissionListener(TestExecutionService testExecutionService, + ExecutionResultProducer executionResultProducer) { this.testExecutionService = testExecutionService; + this.executionResultProducer = executionResultProducer; } @RabbitListener(queues = "${rabbitmq.queue:codehive_queue}") public void handleExecutionJob(ExecutionJob job) { - logger.info("Received execution job: id={}, language={}, type={}", - job.getId(), job.getLanguage(), job.getExecutionType()); + logger.info("[WORKFLOW] RABBITMQ RECEIVE: Received execution job from backend"); + logger.info("[WORKFLOW] === Execution Job Details ==="); + logger.info("[WORKFLOW] ID: {}", job.getId()); + logger.info("[WORKFLOW] Language: {}", job.getLanguage()); + logger.info("[WORKFLOW] Reference Language: {}", job.getReferenceLanguage()); + logger.info("[WORKFLOW] Execution Type: {}", job.getExecutionType()); + logger.info("[WORKFLOW] Source Path: {}", job.getSource()); + logger.info("[WORKFLOW] Reference Path: {}", job.getReference()); + logger.info("[WORKFLOW] Output Path: {}", job.getOutputPath()); + logger.info("[WORKFLOW] Tests Path: {}", job.getTestsPath()); + logger.info("[WORKFLOW] Num Tests: {}", job.getNumTests()); + logger.info("[WORKFLOW] Time Limit (ms): {}", job.getTimeLimitMs()); + logger.info("[WORKFLOW] Memory Limit (MB): {}", job.getMemoryLimitMb()); + logger.info("[WORKFLOW] Comparator Type: {}", job.getComparatorType()); + logger.info("[WORKFLOW] Test Cases: {}", job.getTestCases() != null ? job.getTestCases().size() + " inline tests" : "null"); + logger.info("[WORKFLOW] ============================="); try { // Execute the job and get the report @@ -33,12 +52,17 @@ public void handleExecutionJob(ExecutionJob job) { // Log detailed results logExecutionReport(report); - // TODO: Send result back to backend via RabbitMQ result queue - // This will be implemented when we have a result queue configured + // Send result back to backend via RabbitMQ result queue + executionResultProducer.sendExecutionResult(report); } catch (Exception e) { logger.error("Failed to process execution job: id={}", job.getId(), e); - // TODO: Send error result back to backend + + // Send error result back to backend + ExecutionReport errorReport = new ExecutionReport(job.getId()); + errorReport.setOverallStatus(ExecutionStatus.RTE); + errorReport.setCompilationError("Internal error: " + e.getMessage()); + executionResultProducer.sendExecutionResult(errorReport); } } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/messaging/producer/ExecutionResultProducer.java b/codehive-worker/src/main/java/com/github/codehive/worker/messaging/producer/ExecutionResultProducer.java new file mode 100644 index 0000000..e83e4b9 --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/messaging/producer/ExecutionResultProducer.java @@ -0,0 +1,28 @@ +package com.github.codehive.worker.messaging.producer; + +import com.github.codehive.worker.config.RabbitMQConfig; +import com.github.codehive.worker.model.dto.ExecutionReport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.stereotype.Service; + +@Service +public class ExecutionResultProducer { + private static final Logger logger = LoggerFactory.getLogger(ExecutionResultProducer.class); + + private final RabbitTemplate rabbitTemplate; + + public ExecutionResultProducer(RabbitTemplate rabbitTemplate) { + this.rabbitTemplate = rabbitTemplate; + } + + public void sendExecutionResult(ExecutionReport report) { + logger.info("Sending execution result to queue: executionId={}, status={}", + report.getExecutionId(), report.getOverallStatus()); + + rabbitTemplate.convertAndSend(RabbitMQConfig.RESULT_QUEUE_NAME, report); + + logger.debug("Execution result sent successfully: executionId={}", report.getExecutionId()); + } +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/SandboxExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/SandboxExecutor.java deleted file mode 100644 index fc113e0..0000000 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/SandboxExecutor.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.github.codehive.worker.sandbox; - -public class SandboxExecutor { - -} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java index 8004460..54a9db5 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java @@ -26,7 +26,7 @@ public class JavaExecutor implements LanguageExecutor { private static final Logger logger = LoggerFactory.getLogger(JavaExecutor.class); private final DockerClient dockerClient; - private static final String JAVA_IMAGE = "openjdk:21-slim"; + private static final String JAVA_IMAGE = "eclipse-temurin:21-jdk-ubi10-minimal"; private static final long DEFAULT_TIME_LIMIT_MS = 5000L; private static final long DEFAULT_MEMORY_LIMIT_MB = 256L; diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/service/ObjectStorageService.java b/codehive-worker/src/main/java/com/github/codehive/worker/service/ObjectStorageService.java index c1fc9bf..c0014f6 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/service/ObjectStorageService.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/service/ObjectStorageService.java @@ -3,6 +3,8 @@ import java.io.ByteArrayInputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; + +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import io.minio.GetObjectArgs; import io.minio.MinioClient; @@ -14,7 +16,9 @@ public class ObjectStorageService { private static final Logger logger = LoggerFactory.getLogger(ObjectStorageService.class); private final MinioClient minioClient; - private final String bucketName = System.getProperty("minio.bucketName", "codehive"); + + @Value("${minio.bucketName}") + private String bucketName; public ObjectStorageService(MinioClient minioClient) { this.minioClient = minioClient; diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/service/OutputComparatorService.java b/codehive-worker/src/main/java/com/github/codehive/worker/service/OutputComparatorService.java index c6298b7..315310e 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/service/OutputComparatorService.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/service/OutputComparatorService.java @@ -95,12 +95,6 @@ private ComparisonResult compareExactWithFeedback(String expected, String actual return new ComparisonResult(true, "Passed"); } - /** - * Exact string comparison (trimmed lines, ignoring trailing whitespace) - */ - private boolean compareExact(String expected, String actual) { - return compareExactWithFeedback(expected, actual).matches(); - } /** * Floating point comparison with tolerance and detailed feedback @@ -140,14 +134,6 @@ private ComparisonResult compareFloatingPointWithFeedback(String expected, Strin return new ComparisonResult(true, "Passed"); } - /** - * Floating point comparison with tolerance - * Compares token by token, treating numbers with epsilon tolerance - */ - private boolean compareFloatingPoint(String expected, String actual) { - return compareFloatingPointWithFeedback(expected, actual).matches(); - } - /** * Truncate string for display in feedback */ diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java b/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java index 5da4166..f5f9824 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java @@ -253,15 +253,16 @@ private void uploadTestCaseOutputs(String path, int testCaseNumber, String stdou logger.debug("Uploaded stdout for test case {} to: {}", testCaseNumber, stdoutPath); // Upload stderr - String stderrPath = new StringBuilder() - .append(path) - .append("/tc-") - .append(testCaseNumber) - .append("/stderr.txt") - .toString(); - objectStorageService.upload(stderrPath, stderr != null ? stderr : ""); - logger.debug("Uploaded stderr for test case {} to: {}", testCaseNumber, stderrPath); - + if(stderr != null && !stderr.isEmpty()) { + String stderrPath = new StringBuilder() + .append(path) + .append("/tc-") + .append(testCaseNumber) + .append("/stderr.txt") + .toString(); + objectStorageService.upload(stderrPath, stderr); + logger.debug("Uploaded stderr for test case {} to: {}", testCaseNumber, stderrPath); + } } catch (Exception e) { logger.error("Failed to upload test case outputs for test {}", testCaseNumber, e); } @@ -289,8 +290,9 @@ private String getStatusFeedback(ExecutionStatus status) { private void uploadReport(String outputPath, ExecutionReport report) { try { String jsonReport = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(report); - objectStorageService.upload(outputPath, jsonReport); - logger.info("Uploaded execution report to: {}", outputPath); + String outputPathFinal = outputPath + "report.json"; + objectStorageService.upload(outputPathFinal, jsonReport); + logger.info("Uploaded execution report to: {}", outputPathFinal); } catch (Exception e) { logger.error("Failed to upload execution report", e); } diff --git a/codehive-worker/src/main/resources/application.properties b/codehive-worker/src/main/resources/application.properties index 17744cd..106b10b 100644 --- a/codehive-worker/src/main/resources/application.properties +++ b/codehive-worker/src/main/resources/application.properties @@ -1,13 +1,15 @@ +spring.config.import=optional:file:.env[.properties] spring.application.name=worker # Minio Configuration -minio.url=${MINIO_URL} -minio.accessKey=${MINIO_ACCESS_KEY} -minio.secretKey=${MINIO_SECRET_KEY} -minio.bucketName=${MINIO_BUCKET_NAME} +minio.url=${MINIO_URL:defaultMinioUrl} +minio.accessKey=${MINIO_ACCESS_KEY:defaultAccessKey} +minio.secretKey=${MINIO_SECRET_KEY:defaultSecretKey} +minio.bucketName=${MINIO_BUCKET_NAME:defaultBucketName} # RabbitMQ Configuration -spring.rabbitmq.host=${RABBITMQ_HOST} -spring.rabbitmq.port=${RABBITMQ_PORT} -spring.rabbitmq.username=${RABBITMQ_USERNAME} -spring.rabbitmq.password=${RABBITMQ_PASSWORD} -spring.rabbitmq.queue=${RABBITMQ_QUEUE} \ No newline at end of file +spring.rabbitmq.host=${RABBITMQ_HOST:defaultRabbitHost} +spring.rabbitmq.port=${RABBITMQ_PORT:5672} +spring.rabbitmq.username=${RABBITMQ_USERNAME:defaultRabbitUser} +spring.rabbitmq.password=${RABBITMQ_PASSWORD:defaultRabbitPassword} +spring.rabbitmq.queue=${RABBITMQ_QUEUE:defaultQueue} +spring.rabbitmq.result.queue=${RABBITMQ_RESULT_QUEUE:defaultResultQueue} \ No newline at end of file From 5614a3b2fb080f31ff852ea910f676f6d5cb9b44 Mon Sep 17 00:00:00 2001 From: IrminDev Date: Wed, 28 Jan 2026 20:50:59 -0600 Subject: [PATCH 09/32] Fixes for business logic --- .../listener/ExecutionResultListener.java | 4 +- .../codehive/model/dto/AssignmentDTO.java | 33 +++++++++++---- .../model/dto/queue/ExecutionReport.java | 10 ++--- .../model/dto/queue/TestCaseResult.java | 14 +++---- .../codehive/model/entity/Assignment.java | 42 +++++++++++++++---- .../codehive/model/entity/TestCase.java | 14 ------- .../model/mapper/AssignmentMapper.java | 4 ++ .../codehive/model/mapper/TestCaseMapper.java | 2 - .../repository/ExecutionRepository.java | 9 ---- .../service/ExecutionRequestService.java | 2 +- .../service/ExecutionResultService.java | 4 +- .../listener/SubmissionListener.java | 4 +- .../worker/model/dto/ExecutionReport.java | 16 +++---- .../worker/model/dto/TestCaseResult.java | 14 +++---- 14 files changed, 99 insertions(+), 73 deletions(-) diff --git a/codehive-backend/src/main/java/com/github/codehive/messaging/listener/ExecutionResultListener.java b/codehive-backend/src/main/java/com/github/codehive/messaging/listener/ExecutionResultListener.java index 29384e0..d62d281 100644 --- a/codehive-backend/src/main/java/com/github/codehive/messaging/listener/ExecutionResultListener.java +++ b/codehive-backend/src/main/java/com/github/codehive/messaging/listener/ExecutionResultListener.java @@ -21,9 +21,9 @@ public ExecutionResultListener(ExecutionResultService executionResultService) { public void handleExecutionResult(ExecutionReport report) { logger.info("[WORKFLOW] RABBITMQ RECEIVE: Received execution result from worker - executionId={}, overallStatus={}", report.getExecutionId(), report.getOverallStatus()); - logger.info("[WORKFLOW] RABBITMQ RECEIVE: Result details - passed={}/{}, maxTimeMs={}, maxMemoryKb={}", + logger.info("[WORKFLOW] RABBITMQ RECEIVE: Result details - passed={}/{}, maxTimeMs={}, maxMemoryMb={}", report.getPassedTests(), report.getTotalTests(), - report.getMaxExecutionTimeMs(), report.getMaxMemoryUsedKb()); + report.getMaxExecutionTimeMs(), report.getMaxMemoryUsedMb()); try { executionResultService.processExecutionResult(report); diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/AssignmentDTO.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/AssignmentDTO.java index 291beb5..439c2c0 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/dto/AssignmentDTO.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/AssignmentDTO.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.github.codehive.model.enums.ComparatorType; +import com.github.codehive.model.enums.Language; @JsonInclude(JsonInclude.Include.NON_NULL) public class AssignmentDTO { @@ -14,12 +15,22 @@ public class AssignmentDTO { private List constraints; private List hints; private List tags; - private Integer timeLimitMs; - private Integer memoryLimitMb; + private Long timeLimitMs; + private Long memoryLimitMb; private ComparatorType comparatorType; private LocalDateTime createdAt; private LocalDateTime updatedAt; private LocalDateTime dueDate; + private List allowedLanguages; + private Boolean isActive; + + public Boolean getIsActive() { + return isActive; + } + + public void setIsActive(Boolean isActive) { + this.isActive = isActive; + } public Long getId() { return id; @@ -69,19 +80,19 @@ public void setTags(List tags) { this.tags = tags; } - public Integer getTimeLimitMs() { + public Long getTimeLimitMs() { return timeLimitMs; } - public void setTimeLimitMs(Integer timeLimitMs) { + public void setTimeLimitMs(Long timeLimitMs) { this.timeLimitMs = timeLimitMs; } - public Integer getMemoryLimitMb() { + public Long getMemoryLimitMb() { return memoryLimitMb; } - public void setMemoryLimitMb(Integer memoryLimitMb) { + public void setMemoryLimitMb(Long memoryLimitMb) { this.memoryLimitMb = memoryLimitMb; } @@ -116,4 +127,12 @@ public LocalDateTime getDueDate() { public void setDueDate(LocalDateTime dueDate) { this.dueDate = dueDate; } -} + + public List getAllowedLanguages() { + return allowedLanguages; + } + + public void setAllowedLanguages(List allowedLanguages) { + this.allowedLanguages = allowedLanguages; + } +} \ No newline at end of file diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/ExecutionReport.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/ExecutionReport.java index db4401f..460c6ab 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/ExecutionReport.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/ExecutionReport.java @@ -14,7 +14,7 @@ public class ExecutionReport { private int failedTests; private Long totalExecutionTimeMs; private Long maxExecutionTimeMs; - private Long maxMemoryUsedKb; + private Long maxMemoryUsedMb; private String compilationError; public ExecutionReport() { @@ -90,12 +90,12 @@ public void setMaxExecutionTimeMs(Long maxExecutionTimeMs) { this.maxExecutionTimeMs = maxExecutionTimeMs; } - public Long getMaxMemoryUsedKb() { - return maxMemoryUsedKb; + public Long getMaxMemoryUsedMb() { + return maxMemoryUsedMb; } - public void setMaxMemoryUsedKb(Long maxMemoryUsedKb) { - this.maxMemoryUsedKb = maxMemoryUsedKb; + public void setMaxMemoryUsedMb(Long maxMemoryUsedMb) { + this.maxMemoryUsedMb = maxMemoryUsedMb; } public String getCompilationError() { diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/TestCaseResult.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/TestCaseResult.java index 88b9c06..e748b76 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/TestCaseResult.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/TestCaseResult.java @@ -6,18 +6,18 @@ public class TestCaseResult { private int testCaseNumber; private ExecutionStatus status; private Long executionTimeMs; - private Long memoryUsedKb; + private Long memoryUsedMb; private String feedback; public TestCaseResult() { } public TestCaseResult(int testCaseNumber, ExecutionStatus status, - Long executionTimeMs, Long memoryUsedKb) { + Long executionTimeMs, Long memoryUsedMb) { this.testCaseNumber = testCaseNumber; this.status = status; this.executionTimeMs = executionTimeMs; - this.memoryUsedKb = memoryUsedKb; + this.memoryUsedMb = memoryUsedMb; } public int getTestCaseNumber() { @@ -44,12 +44,12 @@ public void setExecutionTimeMs(Long executionTimeMs) { this.executionTimeMs = executionTimeMs; } - public Long getMemoryUsedKb() { - return memoryUsedKb; + public Long getMemoryUsedMb() { + return memoryUsedMb; } - public void setMemoryUsedKb(Long memoryUsedKb) { - this.memoryUsedKb = memoryUsedKb; + public void setMemoryUsedMb(Long memoryUsedMb) { + this.memoryUsedMb = memoryUsedMb; } public String getFeedback() { diff --git a/codehive-backend/src/main/java/com/github/codehive/model/entity/Assignment.java b/codehive-backend/src/main/java/com/github/codehive/model/entity/Assignment.java index 139ff31..63afdf1 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/entity/Assignment.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/entity/Assignment.java @@ -5,6 +5,7 @@ import java.util.List; import com.github.codehive.model.enums.ComparatorType; +import com.github.codehive.model.enums.Language; import jakarta.persistence.CollectionTable; import jakarta.persistence.Column; @@ -45,12 +46,18 @@ public class Assignment { @CollectionTable(name = "assignment_tags", joinColumns = @JoinColumn(name = "assignment_id")) @Column(name = "tag", length = 50) private List tags = new ArrayList<>(); + + @ElementCollection + @CollectionTable(name = "assignment_allowed_languages", joinColumns = @JoinColumn(name = "assignment_id")) + @Column(name = "language", length = 20) + @Enumerated(EnumType.STRING) + private List allowedLanguages; @Column(nullable = false) - private Integer timeLimitMs; + private Long timeLimitMs; @Column(nullable = false) - private Integer memoryLimitMb; + private Long memoryLimitMb; @Column(nullable = false, length = 20) @Enumerated(EnumType.STRING) @@ -65,15 +72,20 @@ public class Assignment { @Column(nullable = true) private LocalDateTime dueDate; + @Column(nullable = false) + private Boolean isActive; + public Assignment() { this.createdAt = LocalDateTime.now(); this.updatedAt = LocalDateTime.now(); this.constraints = new ArrayList<>(); this.hints = new ArrayList<>(); this.tags = new ArrayList<>(); + this.allowedLanguages = new ArrayList<>(); + this.isActive = true; } - public Assignment(String title, String description, Integer timeLimitMs, Integer memoryLimitMb, ComparatorType comparatorType) { + public Assignment(String title, String description, Long timeLimitMs, Long memoryLimitMb, ComparatorType comparatorType) { this(); this.title = title; this.description = description; @@ -82,6 +94,22 @@ public Assignment(String title, String description, Integer timeLimitMs, Integer this.comparatorType = comparatorType; } + public Boolean getIsActive() { + return isActive; + } + + public void setIsActive(Boolean isActive) { + this.isActive = isActive; + } + + public List getAllowedLanguages() { + return allowedLanguages; + } + + public void setAllowedLanguages(List allowedLanguages) { + this.allowedLanguages = allowedLanguages; + } + public Long getId() { return id; } @@ -130,19 +158,19 @@ public void setTags(List tags) { this.tags = tags; } - public Integer getTimeLimitMs() { + public Long getTimeLimitMs() { return timeLimitMs; } - public void setTimeLimitMs(Integer timeLimitMs) { + public void setTimeLimitMs(Long timeLimitMs) { this.timeLimitMs = timeLimitMs; } - public Integer getMemoryLimitMb() { + public Long getMemoryLimitMb() { return memoryLimitMb; } - public void setMemoryLimitMb(Integer memoryLimitMb) { + public void setMemoryLimitMb(Long memoryLimitMb) { this.memoryLimitMb = memoryLimitMb; } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/entity/TestCase.java b/codehive-backend/src/main/java/com/github/codehive/model/entity/TestCase.java index 623b309..c558bad 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/entity/TestCase.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/entity/TestCase.java @@ -1,7 +1,5 @@ package com.github.codehive.model.entity; -import java.time.LocalDateTime; - import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -28,12 +26,8 @@ public class TestCase { @Column(nullable = false) private Boolean isSample; - - @Column(nullable = false) - private LocalDateTime createdAt; public TestCase() { - this.createdAt = LocalDateTime.now(); this.isSample = false; } @@ -75,12 +69,4 @@ public Boolean getIsSample() { public void setIsSample(Boolean isSample) { this.isSample = isSample; } - - public LocalDateTime getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(LocalDateTime createdAt) { - this.createdAt = createdAt; - } } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/mapper/AssignmentMapper.java b/codehive-backend/src/main/java/com/github/codehive/model/mapper/AssignmentMapper.java index c705c2f..bad1c03 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/mapper/AssignmentMapper.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/mapper/AssignmentMapper.java @@ -23,6 +23,8 @@ public static AssignmentDTO toDTO(Assignment assignment) { dto.setCreatedAt(assignment.getCreatedAt()); dto.setUpdatedAt(assignment.getUpdatedAt()); dto.setDueDate(assignment.getDueDate()); + dto.setAllowedLanguages(assignment.getAllowedLanguages()); + dto.setIsActive(assignment.getIsActive()); return dto; } @@ -43,6 +45,8 @@ public static Assignment toEntity(AssignmentDTO dto) { assignment.setCreatedAt(dto.getCreatedAt()); assignment.setUpdatedAt(dto.getUpdatedAt()); assignment.setDueDate(dto.getDueDate()); + assignment.setAllowedLanguages(dto.getAllowedLanguages()); + assignment.setIsActive(dto.getIsActive()); return assignment; } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/mapper/TestCaseMapper.java b/codehive-backend/src/main/java/com/github/codehive/model/mapper/TestCaseMapper.java index 2902a59..bff2b58 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/mapper/TestCaseMapper.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/mapper/TestCaseMapper.java @@ -16,7 +16,6 @@ public static TestCaseDTO toDTO(TestCase testCase) { testCase.getAssignment().getId() : null); dto.setOrder(testCase.getOrder()); dto.setIsSample(testCase.getIsSample()); - dto.setCreatedAt(testCase.getCreatedAt()); return dto; } @@ -29,7 +28,6 @@ public static TestCase toEntity(TestCaseDTO dto) { // Note: Assignment must be set separately via assignment repository testCase.setOrder(dto.getOrder()); testCase.setIsSample(dto.getIsSample()); - testCase.setCreatedAt(dto.getCreatedAt()); return testCase; } diff --git a/codehive-backend/src/main/java/com/github/codehive/repository/ExecutionRepository.java b/codehive-backend/src/main/java/com/github/codehive/repository/ExecutionRepository.java index f76aa03..11b7d4a 100644 --- a/codehive-backend/src/main/java/com/github/codehive/repository/ExecutionRepository.java +++ b/codehive-backend/src/main/java/com/github/codehive/repository/ExecutionRepository.java @@ -6,17 +6,10 @@ import org.springframework.data.jpa.repository.JpaRepository; import com.github.codehive.model.entity.Execution; -import com.github.codehive.model.entity.Submission; import com.github.codehive.model.enums.ExecutionStatus; import com.github.codehive.model.enums.ExecutionType; public interface ExecutionRepository extends JpaRepository { - List findBySubmission(Submission submission); - - List findBySubmissionId(Long submissionId); - - List findBySubmissionIdOrderByCreatedAtDesc(Long submissionId); - List findByExecutionType(ExecutionType executionType); List findByStatus(ExecutionStatus status); @@ -31,7 +24,5 @@ public interface ExecutionRepository extends JpaRepository { List findBySubmissionIsNull(); - long countBySubmissionId(Long submissionId); - long countByStatus(ExecutionStatus status); } diff --git a/codehive-backend/src/main/java/com/github/codehive/service/ExecutionRequestService.java b/codehive-backend/src/main/java/com/github/codehive/service/ExecutionRequestService.java index f7404a1..a560979 100644 --- a/codehive-backend/src/main/java/com/github/codehive/service/ExecutionRequestService.java +++ b/codehive-backend/src/main/java/com/github/codehive/service/ExecutionRequestService.java @@ -31,7 +31,7 @@ public class ExecutionRequestService { private static final Logger logger = LoggerFactory.getLogger(ExecutionRequestService.class); - private static final Long DEFAULT_TIME_LIMIT_MS = 5000L; + private static final Long DEFAULT_TIME_LIMIT_MS = 1000L; private static final Long DEFAULT_MEMORY_LIMIT_MB = 256L; private final ExecutionRequestProducer executionRequestProducer; diff --git a/codehive-backend/src/main/java/com/github/codehive/service/ExecutionResultService.java b/codehive-backend/src/main/java/com/github/codehive/service/ExecutionResultService.java index ed0ebe0..0bb1f23 100644 --- a/codehive-backend/src/main/java/com/github/codehive/service/ExecutionResultService.java +++ b/codehive-backend/src/main/java/com/github/codehive/service/ExecutionResultService.java @@ -44,8 +44,8 @@ public void processExecutionResult(ExecutionReport report) { } // Update memory (convert KB to MB, use max memory) - if (report.getMaxMemoryUsedKb() != null) { - execution.setMemoryMb(report.getMaxMemoryUsedKb() / 1024); + if (report.getMaxMemoryUsedMb() != null) { + execution.setMemoryMb(report.getMaxMemoryUsedMb()); } executionRepository.save(execution); diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/SubmissionListener.java b/codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/SubmissionListener.java index f1e20a2..2fca803 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/SubmissionListener.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/SubmissionListener.java @@ -85,8 +85,8 @@ private void logExecutionReport(ExecutionReport report) { logger.info("Max Execution Time: {}ms", report.getMaxExecutionTimeMs()); } - if (report.getMaxMemoryUsedKb() != null) { - logger.info("Max Memory Used: {}KB", report.getMaxMemoryUsedKb()); + if (report.getMaxMemoryUsedMb() != null) { + logger.info("Max Memory Used: {}MB", report.getMaxMemoryUsedMb()); } logger.info("========================"); diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionReport.java b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionReport.java index 68165fa..8c60c1d 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionReport.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionReport.java @@ -14,7 +14,7 @@ public class ExecutionReport { private int failedTests; private Long totalExecutionTimeMs; private Long maxExecutionTimeMs; - private Long maxMemoryUsedKb; + private Long maxMemoryUsedMb; private String compilationError; public ExecutionReport() { @@ -53,9 +53,9 @@ private void updateStatistics(TestCaseResult result) { } // Update memory stats - if (result.getMemoryUsedKb() != null) { - if (this.maxMemoryUsedKb == null || result.getMemoryUsedKb() > this.maxMemoryUsedKb) { - this.maxMemoryUsedKb = result.getMemoryUsedKb(); + if (result.getMemoryUsedMb() != null) { + if (this.maxMemoryUsedMb == null || result.getMemoryUsedMb() > this.maxMemoryUsedMb) { + this.maxMemoryUsedMb = result.getMemoryUsedMb(); } } } @@ -157,12 +157,12 @@ public void setMaxExecutionTimeMs(Long maxExecutionTimeMs) { this.maxExecutionTimeMs = maxExecutionTimeMs; } - public Long getMaxMemoryUsedKb() { - return maxMemoryUsedKb; + public Long getMaxMemoryUsedMb() { + return maxMemoryUsedMb; } - public void setMaxMemoryUsedKb(Long maxMemoryUsedKb) { - this.maxMemoryUsedKb = maxMemoryUsedKb; + public void setMaxMemoryUsedMb(Long maxMemoryUsedMb) { + this.maxMemoryUsedMb = maxMemoryUsedMb; } public String getCompilationError() { diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/TestCaseResult.java b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/TestCaseResult.java index b49b65c..d45a082 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/TestCaseResult.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/TestCaseResult.java @@ -6,18 +6,18 @@ public class TestCaseResult { private int testCaseNumber; private ExecutionStatus status; private Long executionTimeMs; - private Long memoryUsedKb; + private Long memoryUsedMb; private String feedback; public TestCaseResult() { } public TestCaseResult(int testCaseNumber, ExecutionStatus status, - Long executionTimeMs, Long memoryUsedKb) { + Long executionTimeMs, Long memoryUsedMb) { this.testCaseNumber = testCaseNumber; this.status = status; this.executionTimeMs = executionTimeMs; - this.memoryUsedKb = memoryUsedKb; + this.memoryUsedMb = memoryUsedMb; } // Getters and setters @@ -45,12 +45,12 @@ public void setExecutionTimeMs(Long executionTimeMs) { this.executionTimeMs = executionTimeMs; } - public Long getMemoryUsedKb() { - return memoryUsedKb; + public Long getMemoryUsedMb() { + return memoryUsedMb; } - public void setMemoryUsedKb(Long memoryUsedKb) { - this.memoryUsedKb = memoryUsedKb; + public void setMemoryUsedMb(Long memoryUsedMb) { + this.memoryUsedMb = memoryUsedMb; } public String getFeedback() { From 03c89015d5ea9bf2ca47e41bddeba0efb6a9ae94 Mon Sep 17 00:00:00 2001 From: IrminDev Date: Wed, 28 Jan 2026 21:13:56 -0600 Subject: [PATCH 10/32] Moved ExecutionResult according to its context --- .../worker/{sandbox => model/dto}/ExecutionResult.java | 2 +- .../com/github/codehive/worker/sandbox/LanguageExecutor.java | 2 ++ .../java/com/github/codehive/worker/sandbox/c/CExecutor.java | 2 +- .../com/github/codehive/worker/sandbox/cpp/CPPExecutor.java | 2 +- .../github/codehive/worker/sandbox/java/JavaExecutor.java | 2 +- .../codehive/worker/sandbox/python/PythonExecutor.java | 2 +- .../github/codehive/worker/service/TestExecutionService.java | 2 +- .../java/com/github/codehive/worker/util/TimeoutUtils.java | 5 ----- 8 files changed, 8 insertions(+), 11 deletions(-) rename codehive-worker/src/main/java/com/github/codehive/worker/{sandbox => model/dto}/ExecutionResult.java (98%) delete mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/util/TimeoutUtils.java diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/ExecutionResult.java b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionResult.java similarity index 98% rename from codehive-worker/src/main/java/com/github/codehive/worker/sandbox/ExecutionResult.java rename to codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionResult.java index 1c1802a..5f06348 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/ExecutionResult.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionResult.java @@ -1,4 +1,4 @@ -package com.github.codehive.worker.sandbox; +package com.github.codehive.worker.model.dto; import com.github.codehive.worker.model.enums.ExecutionStatus; diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/LanguageExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/LanguageExecutor.java index 88fa465..edf3720 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/LanguageExecutor.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/LanguageExecutor.java @@ -2,6 +2,8 @@ import java.io.InputStream; +import com.github.codehive.worker.model.dto.ExecutionResult; + public interface LanguageExecutor { /** * Execute code with given constraints diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/c/CExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/c/CExecutor.java index 67ecb7a..966f84d 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/c/CExecutor.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/c/CExecutor.java @@ -1,6 +1,6 @@ package com.github.codehive.worker.sandbox.c; -import com.github.codehive.worker.sandbox.ExecutionResult; +import com.github.codehive.worker.model.dto.ExecutionResult; import com.github.codehive.worker.sandbox.LanguageExecutor; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.CreateContainerResponse; diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/cpp/CPPExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/cpp/CPPExecutor.java index fd80452..b4bf593 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/cpp/CPPExecutor.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/cpp/CPPExecutor.java @@ -1,6 +1,6 @@ package com.github.codehive.worker.sandbox.cpp; -import com.github.codehive.worker.sandbox.ExecutionResult; +import com.github.codehive.worker.model.dto.ExecutionResult; import com.github.codehive.worker.sandbox.LanguageExecutor; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.CreateContainerResponse; diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java index 54a9db5..28e5ecf 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java @@ -1,6 +1,6 @@ package com.github.codehive.worker.sandbox.java; -import com.github.codehive.worker.sandbox.ExecutionResult; +import com.github.codehive.worker.model.dto.ExecutionResult; import com.github.codehive.worker.sandbox.LanguageExecutor; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.async.ResultCallback.Adapter; diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/python/PythonExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/python/PythonExecutor.java index f7a2923..623c9b5 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/python/PythonExecutor.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/python/PythonExecutor.java @@ -1,6 +1,6 @@ package com.github.codehive.worker.sandbox.python; -import com.github.codehive.worker.sandbox.ExecutionResult; +import com.github.codehive.worker.model.dto.ExecutionResult; import com.github.codehive.worker.sandbox.LanguageExecutor; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.CreateContainerResponse; diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java b/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java index f5f9824..fa648b1 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java @@ -2,11 +2,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.github.codehive.worker.model.dto.ExecutionReport; +import com.github.codehive.worker.model.dto.ExecutionResult; import com.github.codehive.worker.model.dto.TestCaseResult; import com.github.codehive.worker.model.dto.queue.ExecutionJob; import com.github.codehive.worker.model.enums.ExecutionStatus; import com.github.codehive.worker.model.enums.ExecutionType; -import com.github.codehive.worker.sandbox.ExecutionResult; import com.github.codehive.worker.sandbox.LanguageExecutor; import com.github.codehive.worker.sandbox.factory.LanguageExecutorFactory; import org.slf4j.Logger; diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/util/TimeoutUtils.java b/codehive-worker/src/main/java/com/github/codehive/worker/util/TimeoutUtils.java deleted file mode 100644 index 1a00535..0000000 --- a/codehive-worker/src/main/java/com/github/codehive/worker/util/TimeoutUtils.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.github.codehive.worker.util; - -public class TimeoutUtils { - -} From d1845155c047d34952df8a8d7725b27c06b10abf Mon Sep 17 00:00:00 2001 From: IrminDev Date: Wed, 28 Jan 2026 22:42:06 -0600 Subject: [PATCH 11/32] Added max memory use via statistics from docker api --- .../worker/model/dto/ExecutionResult.java | 20 ++++---- .../codehive/worker/sandbox/c/CExecutor.java | 46 +++++++++++++++++- .../worker/sandbox/cpp/CPPExecutor.java | 47 ++++++++++++++++++- .../worker/sandbox/java/JavaExecutor.java | 46 +++++++++++++++++- .../worker/sandbox/python/PythonExecutor.java | 47 ++++++++++++++++++- .../worker/service/TestExecutionService.java | 4 +- 6 files changed, 191 insertions(+), 19 deletions(-) diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionResult.java b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionResult.java index 5f06348..3277110 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionResult.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionResult.java @@ -7,7 +7,7 @@ public class ExecutionResult { private String output; private String errorOutput; private Long executionTimeMs; - private Long memoryUsedKb; + private Long memoryUsedMb; private Integer exitCode; private String compilationError; @@ -15,12 +15,12 @@ public ExecutionResult() { } public ExecutionResult(ExecutionStatus status, String output, String errorOutput, - Long executionTimeMs, Long memoryUsedKb, Integer exitCode) { + Long executionTimeMs, Long memoryUsedMb, Integer exitCode) { this.status = status; this.output = output; this.errorOutput = errorOutput; this.executionTimeMs = executionTimeMs; - this.memoryUsedKb = memoryUsedKb; + this.memoryUsedMb = memoryUsedMb; this.exitCode = exitCode; } @@ -51,16 +51,16 @@ public static ExecutionResult timeLimitExceeded(Long timeLimit) { public static ExecutionResult memoryLimitExceeded(Long memoryUsed) { ExecutionResult result = new ExecutionResult(); result.status = ExecutionStatus.MLE; - result.memoryUsedKb = memoryUsed; + result.memoryUsedMb = memoryUsed; return result; } public static ExecutionResult success(String output, Long executionTime, Long memoryUsed) { ExecutionResult result = new ExecutionResult(); - result.status = ExecutionStatus.AC; // TODO: Will be verified later with test cases + result.status = ExecutionStatus.AC; result.output = output; result.executionTimeMs = executionTime; - result.memoryUsedKb = memoryUsed; + result.memoryUsedMb = memoryUsed; result.exitCode = 0; return result; } @@ -98,12 +98,12 @@ public void setExecutionTimeMs(Long executionTimeMs) { this.executionTimeMs = executionTimeMs; } - public Long getMemoryUsedKb() { - return memoryUsedKb; + public Long getMemoryUsedMb() { + return memoryUsedMb; } - public void setMemoryUsedKb(Long memoryUsedKb) { - this.memoryUsedKb = memoryUsedKb; + public void setMemoryUsedMb(Long memoryUsedMb) { + this.memoryUsedMb = memoryUsedMb; } public Integer getExitCode() { diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/c/CExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/c/CExecutor.java index 966f84d..3290639 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/c/CExecutor.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/c/CExecutor.java @@ -3,10 +3,12 @@ import com.github.codehive.worker.model.dto.ExecutionResult; import com.github.codehive.worker.sandbox.LanguageExecutor; import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.async.ResultCallback; import com.github.dockerjava.api.command.CreateContainerResponse; import com.github.dockerjava.api.command.WaitContainerResultCallback; import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.HostConfig; +import com.github.dockerjava.api.model.Statistics; import com.github.dockerjava.api.model.Volume; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -186,9 +188,9 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit if (exitCode != 0) { return ExecutionResult.runtimeError(stderr, exitCode, executionTime); } + Long memoryUsed = getContainerPeakMemory(containerId); - // TODO: Compare output with expected output for AC/WA verdict - return ExecutionResult.success(stdout, executionTime, 0L); + return ExecutionResult.success(stdout, executionTime, memoryUsed); } catch (Exception e) { logger.error("Execution error", e); @@ -244,4 +246,44 @@ private void deleteDirectory(Path directory) { logger.warn("Failed to delete directory: " + directory, e); } } + + private Long getContainerPeakMemory(String containerId) { + try { + final CountDownLatch latch = new CountDownLatch(1); + final Statistics[] statsHolder = new Statistics[1]; + + dockerClient.statsCmd(containerId) + .withNoStream(true) // Single snapshot + .exec(new ResultCallback.Adapter() { + @Override + public void onNext(Statistics stats) { + statsHolder[0] = stats; + } + + @Override + public void onComplete() { + latch.countDown(); + super.onComplete(); + } + + @Override + public void onError(Throwable throwable) { + latch.countDown(); + super.onError(throwable); + } + }); + + // Wait for completion (with timeout) + latch.await(5, TimeUnit.SECONDS); + Statistics stats = statsHolder[0]; + + if (stats != null && stats.getMemoryStats() != null) { + Long maxUsage = stats.getMemoryStats().getMaxUsage(); + return maxUsage != null ? maxUsage / (1024 * 1024) : 0L; // Bytes to MB + } + } catch (Exception e) { + logger.warn("Failed to get memory stats for container {}", containerId, e); + } + return 0L; // Fallback + } } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/cpp/CPPExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/cpp/CPPExecutor.java index b4bf593..5e68446 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/cpp/CPPExecutor.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/cpp/CPPExecutor.java @@ -3,10 +3,12 @@ import com.github.codehive.worker.model.dto.ExecutionResult; import com.github.codehive.worker.sandbox.LanguageExecutor; import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.async.ResultCallback; import com.github.dockerjava.api.command.CreateContainerResponse; import com.github.dockerjava.api.command.WaitContainerResultCallback; import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.HostConfig; +import com.github.dockerjava.api.model.Statistics; import com.github.dockerjava.api.model.Volume; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -187,8 +189,9 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit return ExecutionResult.runtimeError(stderr, exitCode, executionTime); } - // TODO: Compare output with expected output for AC/WA verdict - return ExecutionResult.success(stdout, executionTime, 0L); + Long memoryUsed = getContainerPeakMemory(containerId); + + return ExecutionResult.success(stdout, executionTime, memoryUsed); } catch (Exception e) { logger.error("Execution error", e); @@ -244,4 +247,44 @@ private void deleteDirectory(Path directory) { logger.warn("Failed to delete directory: " + directory, e); } } + + private Long getContainerPeakMemory(String containerId) { + try { + final CountDownLatch latch = new CountDownLatch(1); + final Statistics[] statsHolder = new Statistics[1]; + + dockerClient.statsCmd(containerId) + .withNoStream(true) // Single snapshot + .exec(new ResultCallback.Adapter() { + @Override + public void onNext(Statistics stats) { + statsHolder[0] = stats; + } + + @Override + public void onComplete() { + latch.countDown(); + super.onComplete(); + } + + @Override + public void onError(Throwable throwable) { + latch.countDown(); + super.onError(throwable); + } + }); + + // Wait for completion (with timeout) + latch.await(5, TimeUnit.SECONDS); + Statistics stats = statsHolder[0]; + + if (stats != null && stats.getMemoryStats() != null) { + Long maxUsage = stats.getMemoryStats().getMaxUsage(); + return maxUsage != null ? maxUsage / (1024 * 1024) : 0L; // Bytes to MB + } + } catch (Exception e) { + logger.warn("Failed to get memory stats for container {}", containerId, e); + } + return 0L; // Fallback + } } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java index 28e5ecf..4558d67 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java @@ -3,12 +3,14 @@ import com.github.codehive.worker.model.dto.ExecutionResult; import com.github.codehive.worker.sandbox.LanguageExecutor; import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.async.ResultCallback; import com.github.dockerjava.api.async.ResultCallback.Adapter; import com.github.dockerjava.api.command.CreateContainerResponse; import com.github.dockerjava.api.command.WaitContainerResultCallback; import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.Frame; import com.github.dockerjava.api.model.HostConfig; +import com.github.dockerjava.api.model.Statistics; import com.github.dockerjava.api.model.Volume; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -188,8 +190,10 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit if (exitCode != 0) { return ExecutionResult.runtimeError(stderr, exitCode, executionTime); } + + Long memoryUsed = getContainerPeakMemory(containerId); - return ExecutionResult.success(stdout, executionTime, 0L); + return ExecutionResult.success(stdout, executionTime, memoryUsed); } catch (Exception e) { logger.error("Execution error", e); @@ -245,4 +249,44 @@ private void deleteDirectory(Path directory) { logger.warn("Failed to delete directory: " + directory, e); } } + + private Long getContainerPeakMemory(String containerId) { + try { + final CountDownLatch latch = new CountDownLatch(1); + final Statistics[] statsHolder = new Statistics[1]; // Array to allow mutation + + dockerClient.statsCmd(containerId) + .withNoStream(true) // Single snapshot + .exec(new ResultCallback.Adapter() { + @Override + public void onNext(Statistics stats) { + statsHolder[0] = stats; + } + + @Override + public void onComplete() { + latch.countDown(); + super.onComplete(); + } + + @Override + public void onError(Throwable throwable) { + latch.countDown(); + super.onError(throwable); + } + }); + + // Wait for completion (with timeout) + latch.await(5, TimeUnit.SECONDS); + Statistics stats = statsHolder[0]; + + if (stats != null && stats.getMemoryStats() != null) { + Long maxUsage = stats.getMemoryStats().getMaxUsage(); + return maxUsage != null ? maxUsage / (1024 * 1024) : 0L; // Bytes to MB + } + } catch (Exception e) { + logger.warn("Failed to get memory stats for container {}", containerId, e); + } + return 0L; // Fallback + } } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/python/PythonExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/python/PythonExecutor.java index 623c9b5..360660c 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/python/PythonExecutor.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/python/PythonExecutor.java @@ -3,10 +3,12 @@ import com.github.codehive.worker.model.dto.ExecutionResult; import com.github.codehive.worker.sandbox.LanguageExecutor; import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.async.ResultCallback; import com.github.dockerjava.api.command.CreateContainerResponse; import com.github.dockerjava.api.command.WaitContainerResultCallback; import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.HostConfig; +import com.github.dockerjava.api.model.Statistics; import com.github.dockerjava.api.model.Volume; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -143,8 +145,9 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit return ExecutionResult.runtimeError(stderr, exitCode, executionTime); } - // TODO: Compare output with expected output for AC/WA verdict - return ExecutionResult.success(stdout, executionTime, 0L); + Long memoryUsed = getContainerPeakMemory(containerId); + + return ExecutionResult.success(stdout, executionTime, memoryUsed); } catch (Exception e) { logger.error("Execution error", e); @@ -200,4 +203,44 @@ private void deleteDirectory(Path directory) { logger.warn("Failed to delete directory: " + directory, e); } } + + private Long getContainerPeakMemory(String containerId) { + try { + final CountDownLatch latch = new CountDownLatch(1); + final Statistics[] statsHolder = new Statistics[1]; + + dockerClient.statsCmd(containerId) + .withNoStream(true) // Single snapshot + .exec(new ResultCallback.Adapter() { + @Override + public void onNext(Statistics stats) { + statsHolder[0] = stats; + } + + @Override + public void onComplete() { + latch.countDown(); + super.onComplete(); + } + + @Override + public void onError(Throwable throwable) { + latch.countDown(); + super.onError(throwable); + } + }); + + // Wait for completion (with timeout) + latch.await(5, TimeUnit.SECONDS); + Statistics stats = statsHolder[0]; + + if (stats != null && stats.getMemoryStats() != null) { + Long maxUsage = stats.getMemoryStats().getMaxUsage(); + return maxUsage != null ? maxUsage / (1024 * 1024) : 0L; // Bytes to MB + } + } catch (Exception e) { + logger.warn("Failed to get memory stats for container {}", containerId, e); + } + return 0L; // Fallback + } } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java b/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java index fa648b1..6ffd55e 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java @@ -117,7 +117,7 @@ private void executeDefinitiveTests(ExecutionJob job, LanguageExecutor executor, i, result.getStatus(), result.getExecutionTimeMs(), - result.getMemoryUsedKb() + result.getMemoryUsedMb() ); // Compare outputs if execution was successful @@ -202,7 +202,7 @@ private void executePracticeTests(ExecutionJob job, LanguageExecutor executor, E testNumber, result.getStatus(), result.getExecutionTimeMs(), - result.getMemoryUsedKb() + result.getMemoryUsedMb() ); // Compare outputs if execution was successful From 7a07047646abc46a439a642c8cab782983987ed9 Mon Sep 17 00:00:00 2001 From: IrminDev Date: Mon, 16 Feb 2026 19:20:10 -0600 Subject: [PATCH 12/32] Minor fixes --- ...ionListener.java => ExecutionRequestListener.java} | 6 +++--- .../worker/service/OutputComparatorService.java | 11 ----------- 2 files changed, 3 insertions(+), 14 deletions(-) rename codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/{SubmissionListener.java => ExecutionRequestListener.java} (96%) diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/SubmissionListener.java b/codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/ExecutionRequestListener.java similarity index 96% rename from codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/SubmissionListener.java rename to codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/ExecutionRequestListener.java index 2fca803..a5ca7ea 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/SubmissionListener.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/ExecutionRequestListener.java @@ -11,13 +11,13 @@ import org.springframework.stereotype.Component; @Component -public class SubmissionListener { - private static final Logger logger = LoggerFactory.getLogger(SubmissionListener.class); +public class ExecutionRequestListener { + private static final Logger logger = LoggerFactory.getLogger(ExecutionRequestListener.class); private final TestExecutionService testExecutionService; private final ExecutionResultProducer executionResultProducer; - public SubmissionListener(TestExecutionService testExecutionService, + public ExecutionRequestListener(TestExecutionService testExecutionService, ExecutionResultProducer executionResultProducer) { this.testExecutionService = testExecutionService; this.executionResultProducer = executionResultProducer; diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/service/OutputComparatorService.java b/codehive-worker/src/main/java/com/github/codehive/worker/service/OutputComparatorService.java index 315310e..45462f2 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/service/OutputComparatorService.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/service/OutputComparatorService.java @@ -58,17 +58,6 @@ public ComparisonResult compareWithFeedback(String expected, String actual, Comp } } - /** - * Compare two outputs based on the comparator type (legacy method) - * @param expected Expected output - * @param actual Actual output from execution - * @param comparatorType Type of comparison to perform - * @return true if outputs match, false otherwise - */ - public boolean compare(String expected, String actual, ComparatorType comparatorType) { - return compareWithFeedback(expected, actual, comparatorType).matches(); - } - /** * Exact string comparison with detailed feedback */ From cabfd64f71ae7c41759dc999a3faaae978471fc6 Mon Sep 17 00:00:00 2001 From: IrminDev Date: Sun, 12 Apr 2026 17:52:21 -0600 Subject: [PATCH 13/32] Updated AGENTS.md file optimizing the token consumption --- AGENTS.md | 1542 +++------------------------- llms/backend/OVERVIEW.md | 80 ++ llms/backend/auth/README.md | 73 ++ llms/backend/controller/README.md | 56 + llms/backend/executions/README.md | 75 ++ llms/backend/messaging/README.md | 51 + llms/backend/model/README.md | 154 +++ llms/backend/security/README.md | 62 ++ llms/backend/service/README.md | 97 ++ llms/frontend/OVERVIEW.md | 67 ++ llms/frontend/admin/README.md | 66 ++ llms/frontend/components/README.md | 54 + llms/frontend/professor/README.md | 14 + llms/frontend/routes/README.md | 56 + llms/frontend/services/README.md | 64 ++ llms/frontend/student/README.md | 14 + llms/worker/OVERVIEW.md | 71 ++ llms/worker/comparator/README.md | 50 + llms/worker/execution/README.md | 74 ++ llms/worker/messaging/README.md | 62 ++ llms/worker/sandbox/README.md | 91 ++ 21 files changed, 1464 insertions(+), 1409 deletions(-) create mode 100644 llms/backend/OVERVIEW.md create mode 100644 llms/backend/auth/README.md create mode 100644 llms/backend/controller/README.md create mode 100644 llms/backend/executions/README.md create mode 100644 llms/backend/messaging/README.md create mode 100644 llms/backend/model/README.md create mode 100644 llms/backend/security/README.md create mode 100644 llms/backend/service/README.md create mode 100644 llms/frontend/OVERVIEW.md create mode 100644 llms/frontend/admin/README.md create mode 100644 llms/frontend/components/README.md create mode 100644 llms/frontend/professor/README.md create mode 100644 llms/frontend/routes/README.md create mode 100644 llms/frontend/services/README.md create mode 100644 llms/frontend/student/README.md create mode 100644 llms/worker/OVERVIEW.md create mode 100644 llms/worker/comparator/README.md create mode 100644 llms/worker/execution/README.md create mode 100644 llms/worker/messaging/README.md create mode 100644 llms/worker/sandbox/README.md diff --git a/AGENTS.md b/AGENTS.md index 2673bc8..d80cf66 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,1419 +1,143 @@ -# AGENTS.md - CodeHive Development Guide +# AGENTS.md - CodeHive Root Instructions -## Project Overview - -**CodeHive** is a collaborative educational platform that allows teachers to create groups with students who can deliver code by editing it directly from the platform. This is a full-stack monorepo with three main components: - -- **codehive-backend**: Spring Boot 3.5.6 REST API (Java 21) -- **codehive-frontend**: React Router v7 SPA (TypeScript, React 19) -- **codehive-worker**: Spring Boot 4.0.1 worker service for sandboxed code execution (Java 21) - -Current development focuses on authentication, user management, and the foundation for code execution infrastructure with RabbitMQ integration. - ---- - -## Repository Structure - -``` -CodeHive/ -├── codehive-backend/ # Main Spring Boot API -│ ├── src/main/java/com/github/codehive/ -│ │ ├── config/ # Spring configuration (Security, OpenAPI, Cache, RabbitMQ) -│ │ ├── controller/ # REST controllers (Auth, RecoveryPassword) -│ │ ├── messaging/ -│ │ │ └── listener/ # RabbitMQ message listeners (ExecutionResultListener) -│ │ ├── model/ -│ │ │ ├── entity/ # JPA entities (User, PasswordResetToken) -│ │ │ ├── dto/ # Data Transfer Objects (UserDTO) -│ │ │ ├── request/ # Request POJOs organized by domain (auth/, recovery/) -│ │ │ ├── response/ # Response POJOs with inheritance hierarchy -│ │ │ ├── exception/ # Custom exceptions organized by domain -│ │ │ ├── enums/ # Enums (Role, Scope) -│ │ │ └── mapper/ # Entity <-> DTO mappers -│ │ ├── repository/ # JPA repositories -│ │ ├── service/ # Business logic layer -│ │ ├── security/ # JWT filter, UserDetailsService implementation -│ │ ├── ratelimit/ # Bucket4j-based rate limiting with AOP -│ │ └── utils/ # Utility classes (JwtUtil) -│ ├── src/test/java/ # Unit and integration tests (121 tests, 70%+ coverage) -│ ├── build.gradle.kts # Gradle build configuration -│ ├── docker-compose.yaml # PostgreSQL + RabbitMQ containers -│ └── .env # Environment variables (DB, JWT, Mail) -│ -├── codehive-worker/ # Code execution worker service -│ ├── src/main/java/com/github/codehive/worker/ -│ │ ├── config/ # RabbitMQ, Docker, and MinIO configuration -│ │ ├── messaging/ -│ │ │ ├── listener/ # RabbitMQ message listeners (SubmissionListener) -│ │ │ └── producer/ # RabbitMQ message producers (ExecutionResultProducer) -│ │ ├── model/ -│ │ │ ├── dto/queue/ # ExecutionJob DTO -│ │ │ └── enums/ # Language, ExecutionStatus, ExecutionType, ComparatorType -│ │ ├── sandbox/ # Code execution in isolated Docker containers -│ │ │ ├── java/ # JavaExecutor for Java code -│ │ │ ├── python/ # PythonExecutor for Python code -│ │ │ ├── c/ # CExecutor for C code -│ │ │ ├── cpp/ # CPPExecutor for C++ code -│ │ │ ├── factory/ # LanguageExecutorFactory for strategy pattern -│ │ │ ├── ExecutionResult.java -│ │ │ └── LanguageExecutor.java (interface) -│ │ ├── service/ # ObjectStorageService for MinIO -│ │ └── util/ # Utility classes (ObjectKeyBuilder, TimeoutUtils) -│ ├── src/test/java/ # Tests with Testcontainers configuration -│ ├── build.gradle.kts # Spring Boot 4.0.1, RabbitMQ, Docker Java client, MinIO -│ └── settings.gradle.kts -│ -├── codehive-frontend/ # React Router v7 frontend -│ ├── app/ -│ │ ├── routes/ # Page routes (home, login, signup, etc.) -│ │ ├── components/ # Reusable UI components -│ │ ├── services/ # API service classes (AuthService, RecoveryPasswordService) -│ │ ├── types/ # TypeScript types (request, response, model) -│ │ ├── context/ # React contexts (ThemeContext) -│ │ └── pages/ # Page components -│ ├── package.json # npm scripts and dependencies -│ └── vite.config.ts # Vite + React Router + Tailwind CSS -│ -├── .github/workflows/ # CI/CD pipelines -│ └── backend-ci.yml # Automated testing, coverage, build -└── PR_template.md # Pull request template -``` - ---- - -## Essential Commands - -### Backend (codehive-backend) - -**Prerequisites:** -- Java 21 -- Docker & Docker Compose (for PostgreSQL + RabbitMQ) - -**Development:** -```bash -cd codehive-backend - -# Start infrastructure (PostgreSQL + RabbitMQ) -docker-compose up -d - -# Run the application (port 8080) -./gradlew bootRun - -# Build -./gradlew build - -# Build without tests -./gradlew build -x test -``` - -**Testing:** -```bash -# Run all tests (121 tests: 71 unit + 50 integration) -./gradlew test - -# Run unit tests only -./gradlew test --tests "*Test" --exclude-tests "*IntegrationTest" - -# Run integration tests only -./gradlew test --tests "*IntegrationTest" - -# Generate coverage report (target: 70%+) -./gradlew test jacocoTestReport - -# View coverage report -open build/reports/jacoco/test/html/index.html -``` - -**Database:** -```bash -# Docker containers defined in docker-compose.yaml -docker-compose up -d postgres # PostgreSQL 18.1-alpine (port 5432) -docker-compose up -d rabbitmq # RabbitMQ 4.2.2-management (ports 5672, 15672) -docker-compose down -``` - -**API Documentation:** -- Swagger UI: http://localhost:8080/swagger-ui.html -- OpenAPI JSON: http://localhost:8080/v3/api-docs +## Purpose +This file contains only repository-wide guidance. +Implementation specifics must live in the docs under llms/. -### Worker (codehive-worker) - -**Prerequisites:** +## Project Overview +CodeHive is a full-stack monorepo for collaborative programming education. + +Main applications: +- codehive-backend: Spring Boot REST API (Java 21) +- codehive-frontend: React Router v7 SPA (TypeScript) +- codehive-worker: Spring Boot worker for sandboxed execution (Java 21) + +The llms folder is the source of truth for implementation-level guidance: +- llms/backend/OVERVIEW.md +- llms/frontend/OVERVIEW.md +- llms/worker/OVERVIEW.md + +Subfolder docs under each area (for example auth, service, messaging, sandbox, routes, components) contain domain-specific instructions. + +## llms Folder Structure +Use this map to load only the docs relevant to the task. + +``` +llms/ +├── backend/ +│ ├── OVERVIEW.md +│ ├── auth/ +│ ├── controller/ +│ ├── executions/ +│ ├── messaging/ +│ ├── model/ +│ ├── security/ +│ └── service/ +├── frontend/ +│ ├── OVERVIEW.md +│ ├── admin/ +│ ├── components/ +│ ├── professor/ +│ ├── routes/ +│ ├── services/ +│ └── student/ +└── worker/ + ├── OVERVIEW.md + ├── comparator/ + ├── execution/ + ├── messaging/ + └── sandbox/ +``` + +## Quick Doc Lookup +Open only the area needed for the current change. + +- Authentication, JWT, password recovery, user registration: llms/backend/auth/ +- HTTP endpoint design and controller behavior: llms/backend/controller/ +- Data model, entities, DTOs, requests, responses, exceptions: llms/backend/model/ +- Backend queue flow and contracts: llms/backend/messaging/ +- Execution request/result orchestration in backend: llms/backend/executions/ +- Backend service-layer orchestration: llms/backend/service/ +- Backend security config and filters: llms/backend/security/ + +- Frontend route mapping and route-level composition: llms/frontend/routes/ +- Frontend API clients and request/response handling: llms/frontend/services/ +- Reusable UI and guard components: llms/frontend/components/ +- Admin pages and workflows: llms/frontend/admin/ +- Professor and student areas: llms/frontend/professor/, llms/frontend/student/ (placeholders until dedicated pages exist) + +- Worker execution lifecycle and result aggregation: llms/worker/execution/ +- Worker output comparison and verdict logic: llms/worker/comparator/ +- Worker queue listener/producer behavior: llms/worker/messaging/ +- Worker sandbox executors and isolation constraints: llms/worker/sandbox/ + +## Development Environment +General prerequisites: - Java 21 -- Docker (for executing code in isolated containers) -- RabbitMQ running (via backend's docker-compose) - -**Development:** -```bash -cd codehive-worker - -# Build -./gradlew build - -# Run (connects to RabbitMQ on localhost:5672) -./gradlew bootRun - -# Test -./gradlew test -``` - -**Architecture:** -- Listens to RabbitMQ queues for code submission messages -- Executes code in isolated Docker containers per language -- Supports Java, Python, C, and C++ -- Uses Docker Java API for container management -- Enforces execution timeouts and resource limits - -### Frontend (codehive-frontend) - -**Prerequisites:** - Node.js 18+ +- Docker and Docker Compose -**Development:** -```bash -cd codehive-frontend - -# Install dependencies -npm install - -# Start dev server (port 3000 or 5173) -npm run dev - -# Build for production -npm run build - -# Start production server -npm run start - -# Type checking -npm run typecheck -``` - -**Environment Variables:** -- Set `VITE_API_URL` in `.env` (defaults to http://localhost:8080) - ---- - -## Code Patterns & Conventions - -### Backend Java Patterns - -#### Package Organization -- **Domain-based subpackages**: `model/request/auth/`, `model/exception/recovery/` -- **Consistent naming**: `{Action}Request`, `{Domain}Service`, `{Entity}Repository` -- **Clear separation**: Controllers → Services → Repositories → Entities - -#### Request/Response Objects - -**Requests** (`model/request/{domain}/`): -```java -// Pattern: {Action}Request in domain subfolder -// Example: model/request/auth/LoginRequest.java -public class LoginRequest { - @NotBlank(message = "Email is required") - private String identifier; - - @Size(min = 6, message = "Password must be at least 6 characters") - private String password; - - // No-arg constructor + parameterized constructor - // Standard getters/setters -} -``` - -**Responses** (`model/response/`): -- **Base class**: `ApiResponse` (abstract, has `success` + `message`) -- **Generic wrapper**: `SuccessResponse` with `data` field -- **Domain-specific**: `AuthResponse` in `response/auth/` subfolder -- **All use**: `@JsonInclude(JsonInclude.Include.NON_NULL)` - -```java -// Example: model/response/SuccessResponse.java -@JsonInclude(JsonInclude.Include.NON_NULL) -public class SuccessResponse extends ApiResponse { - private final T data; - - public SuccessResponse(String message, T data) { - super(true, message); - this.data = data; - } -} -``` - -#### Custom Exceptions - -**Pattern**: Domain-organized exceptions extending `RuntimeException` -```java -// Location: model/exception/{domain}/{Description}Exception.java -// Example: model/exception/auth/IncorrectCredentialsException.java -public class IncorrectCredentialsException extends RuntimeException { - // Always include 4 constructors: - public IncorrectCredentialsException(String message) { super(message); } - public IncorrectCredentialsException(String message, Throwable cause) { super(message, cause); } - public IncorrectCredentialsException(Throwable cause) { super(cause); } - public IncorrectCredentialsException() { super("The provided credentials are incorrect."); } -} -``` - -**Exception Handling**: `GlobalExceptionHandler` with `@RestControllerAdvice` -- **401**: Auth failures (`IncorrectCredentialsException`, `InvalidJWTException`) -- **404**: Not found (`EntityNotFoundException`, `TokenNotFoundException`) -- **409**: Conflicts (`AlreadyRegisteredEmailException`) -- **400**: Validation errors (`MethodArgumentNotValidException`) -- **429**: Rate limit exceeded (`RateLimitExceededException`) -- **500**: Generic runtime exceptions - -#### Controllers - -**Pattern**: REST controllers with OpenAPI annotations + rate limiting -```java -@RestController -@RequestMapping("/api/auth") -@Tag(name = "Authentication", description = "Authentication management APIs") -public class AuthController { - private final AuthService authService; - - // Constructor injection (no @Autowired needed) - public AuthController(AuthService authService) { - this.authService = authService; - } - - @Operation(summary = "User login", description = "Authenticate user...") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Login successful", - content = @Content(schema = @Schema(implementation = SuccessResponse.class))), - @ApiResponse(responseCode = "401", description = "Invalid credentials", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))) - }) - @RateLimit(limit = 5, duration = 60, message = "Too many login attempts. Please try again in 1 minute.") - @PostMapping("/login") - public ResponseEntity> login(@Valid @RequestBody LoginRequest request) { - AuthResponse authResponse = authService.login(request); - return ResponseEntity.ok(new SuccessResponse<>("Login successful", authResponse)); - } -} -``` - -#### Services - -**Pattern**: Business logic with `@Service` and `@Transactional` -```java -@Service -public class AuthService { - private final UserRepository userRepository; - private final PasswordEncoder passwordEncoder; - private final JwtUtil jwtUtil; - - // Constructor injection - public AuthService(UserRepository userRepository, PasswordEncoder passwordEncoder, JwtUtil jwtUtil) { - this.userRepository = userRepository; - this.passwordEncoder = passwordEncoder; - this.jwtUtil = jwtUtil; - } - - @Transactional(readOnly = true) - public AuthResponse login(LoginRequest request) { - // Business logic - // Throw custom exceptions on error - // Return DTOs or response objects - } -} -``` - -#### Repositories - -**Pattern**: Spring Data JPA interfaces -```java -public interface UserRepository extends JpaRepository { - Optional findByEmail(String email); - Optional findByEnrollmentNumber(String enrollmentNumber); -} -``` - -#### Rate Limiting - -**Implementation**: Annotation-based AOP with Bucket4j - -1. **Annotation** (`ratelimit/RateLimit.java`): -```java -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface RateLimit { - int limit(); // Max requests - int duration(); // Time window in seconds - String message(); // Error message -} -``` - -2. **Usage on controllers**: -```java -@RateLimit(limit = 5, duration = 60, message = "Too many login attempts...") -@PostMapping("/login") -``` - -3. **Aspect intercepts** and checks `RateLimitService` (IP-based buckets) - -#### RabbitMQ Messaging - -**Backend RabbitMQ Configuration** (`config/RabbitConfig.java`): -```java -@Configuration -public class RabbitConfig { - public static final String QUEUE_NAME = "codehive_queue"; // For sending jobs to worker - public static final String RESULT_QUEUE_NAME = "codehive_result_queue"; // For receiving results - - @Bean - Queue executionQueue() { return new Queue(QUEUE_NAME, true); } - - @Bean - Queue resultQueue() { return new Queue(RESULT_QUEUE_NAME, true); } - - @Bean - public MessageConverter jsonMessageConverter() { - return new Jackson2JsonMessageConverter(); - } -} -``` - -**Execution Result Listener** (`messaging/listener/ExecutionResultListener.java`): -```java -@Component -public class ExecutionResultListener { - private final ExecutionResultService executionResultService; - - @RabbitListener(queues = "${rabbitmq.result.queue:codehive_result_queue}") - public void handleExecutionResult(ExecutionReport report) { - logger.info("Received execution result: executionId={}, status={}", - report.getExecutionId(), report.getOverallStatus()); - executionResultService.processExecutionResult(report); - } -} -``` - -**Execution Result Service** (`service/ExecutionResultService.java`): -```java -@Service -public class ExecutionResultService { - private final ExecutionRepository executionRepository; - - @Transactional - public void processExecutionResult(ExecutionReport report) { - Execution execution = executionRepository.findById(report.getExecutionId()) - .orElse(null); - - if (execution == null) { - logger.warn("Execution not found: id={}", report.getExecutionId()); - return; - } - - execution.setStatus(report.getOverallStatus()); - execution.setTimeMs(report.getMaxExecutionTimeMs()); - execution.setMemoryMb(report.getMaxMemoryUsedKb() / 1024); // KB to MB - - executionRepository.save(execution); - } -} -``` - -**Queue DTOs** (`model/dto/queue/`): -- `ExecutionJob.java`: Sent to worker (source path, language, test config, limits) -- `ExecutionReport.java`: Received from worker (status, test results, statistics) -- `TestCaseResult.java`: Individual test case result (status, time, memory, feedback) - -#### Testing Conventions - -**Unit Tests** (`*Test.java`): -- `@ExtendWith(MockitoExtension.class)` -- `@Mock` for dependencies, `@InjectMocks` for service under test -- Nested test classes with `@Nested` and `@DisplayName` -- AssertJ assertions: `assertThat(...).isEqualTo(...)`, `assertThatThrownBy(...)` - -**Integration Tests** (`*IntegrationTest.java`): -- `@SpringBootTest` + `@AutoConfigureMockMvc` + `@Transactional` -- `@ActiveProfiles("test")` (uses H2 in-memory database) -- `MockMvc` for HTTP testing -- Real database interactions with test data setup in `@BeforeEach` - -**Test Structure**: -```java -@ExtendWith(MockitoExtension.class) -@DisplayName("AuthService Unit") -class AuthServiceTest { - @Mock private UserRepository userRepository; - @InjectMocks private AuthService authService; - - @Nested - @DisplayName("login()") - class LoginTests { - @Test - @DisplayName("Returns AuthResponse for valid credentials") - void login_WithValidCredentials_ReturnsAuthResponse() { - // Given - // When - // Then - } - } -} -``` - -### Worker Java Patterns - -#### Package Organization -- **config/**: Spring configuration beans (RabbitMQ, Docker client, MinIO) -- **messaging/**: RabbitMQ message handling - - **listener/**: `@RabbitListener` components (SubmissionListener) - - **producer/**: Message producers (ExecutionResultProducer) -- **model/**: Data models - - **dto/**: DTOs (ExecutionReport, TestCaseResult) - - **dto/queue/**: Message DTOs (ExecutionJob) - - **enums/**: Enums (Language, ExecutionStatus, ExecutionType, ComparatorType) -- **sandbox/**: Code execution in Docker containers - - **{language}/**: Language-specific executor implementations (java, python, c, cpp) - - **factory/**: `LanguageExecutorFactory` for strategy pattern - - Core interfaces and result models -- **service/**: Business logic (ObjectStorageService for MinIO) -- **util/**: Utility classes (ObjectKeyBuilder, TimeoutUtils) - -#### Language Executor Pattern - -**Interface** (`sandbox/LanguageExecutor.java`): -```java -public interface LanguageExecutor { - /** - * Execute code with given constraints - * @param sourceCode The source code input stream from MinIO - * @param testInput The test input stream from MinIO (can be null) - * @param timeLimitMs Time limit in milliseconds - * @param memoryLimitMb Memory limit in megabytes - * @return ExecutionResult with verdict and execution details - */ - ExecutionResult execute(InputStream sourceCode, InputStream testInput, - Long timeLimitMs, Long memoryLimitMb) throws Exception; -} -``` - -**Implementation** (e.g., `sandbox/java/JavaExecutor.java`): -```java -@Component("JAVA") // Bean name matches Language enum -public class JavaExecutor implements LanguageExecutor { - private final DockerClient dockerClient; - private static final String JAVA_IMAGE = "openjdk:21-slim"; - - @Override - public ExecutionResult execute(InputStream sourceCode, InputStream testInput, - Long timeLimitMs, Long memoryLimitMb) { - // 1. Create temp directory and save source/input files - // 2. Compile in Docker container (separate from execution) - // 3. Execute in isolated container with resource limits - // 4. Monitor for TLE, MLE, RTE, CE - // 5. Capture output and cleanup - // 6. Return ExecutionResult with status (TLE/MLE/RTE/CE/AC) - } -} -``` - -**Supported Languages**: -- **Java**: `@Component("JAVA")` - JavaExecutor (openjdk:21-slim) -- **Python**: `@Component("PYTHON")` - PythonExecutor (python:3.11-slim) -- **C**: `@Component("C")` - CExecutor (gcc:latest) -- **C++**: `@Component("CPP")` - CPPExecutor (gcc:latest with g++) - -**Factory Pattern** (`sandbox/factory/LanguageExecutorFactory.java`): -```java -@Component -public class LanguageExecutorFactory { - private final Map executors; - - // Spring injects all LanguageExecutor beans by bean name - public LanguageExecutorFactory(Map executors) { - this.executors = executors; - } - - public LanguageExecutor getExecutor(Language language) { - return executors.get(language.name()); // "JAVA", "PYTHON", "C", "CPP" - } -} -``` - -**Execution Result**: -```java -public class ExecutionResult { - private ExecutionStatus status; // TLE, MLE, RTE, CE, WA, AC (enum) - private String output; // stdout - private String errorOutput; // stderr - private Long executionTimeMs; // Execution time - private Long memoryUsedKb; // Memory used (best effort) - private Integer exitCode; // Process exit code - private String compilationError; // Compilation errors - - // Factory methods: compilationError(), runtimeError(), - // timeLimitExceeded(), memoryLimitExceeded(), success() -} -``` - -**Verdict Detection**: -- **CE (Compilation Error)**: Non-zero exit code during compilation phase -- **TLE (Time Limit Exceeded)**: Execution exceeds `timeLimitMs` (detected via Future timeout) -- **MLE (Memory Limit Exceeded)**: Container killed by Docker (exit code 137) -- **RTE (Runtime Error)**: Non-zero exit code during execution -- **AC (Accepted)**: Exit code 0 with successful execution (TODO: output comparison for AC/WA) - -#### Messaging Pattern - -**Message DTO** (`model/dto/queue/ExecutionJob.java`): -```java -public class ExecutionJob { - private Long id; // Execution ID - private String source; // MinIO path to source code - private String reference; // MinIO path to reference solution - private Language language; // Student code language: JAVA, PYTHON, C, CPP - private Language referenceLanguage; // Reference solution language - private ExecutionType executionType; // PRACTICE or DEFINITIVE - private List testCases; // Inline test inputs (PRACTICE mode) - private String outputPath; // MinIO path to store execution report JSON - private Long timeLimitMs; // Time limit in milliseconds - private Long memoryLimitMb; // Memory limit in megabytes - private Integer numTests; // Number of test cases (DEFINITIVE mode) - private String testsPath; // Base path for test files (DEFINITIVE mode) - private ComparatorType comparatorType; // EXACT_MATCH or FLOATING_POINT -} -``` - -**MinIO Path Structure**: -- **Test inputs**: `test-suites/assignments/{assignment-id}/tc-{tc-id}/tc-{tc-id}.in` -- **Expected outputs**: `test-suites/assignments/{assignment-id}/tc-{tc-id}/tc-{tc-id}.out` -- **Submissions**: `submissions/groups/{group-id}/assignments/{assignment-id}/submission-{submission-id}/Main.{ext}` -- **Practice executions**: `test-execution/execution-{id}/Main.{ext}` - -**Execution Types**: -- **DEFINITIVE**: Run against stored test cases (1 to numTests) from `testsPath`, compare with `.out` files -- **PRACTICE**: Run against inline test cases, compare with reference solution output - -**Listener** (`messaging/listener/SubmissionListener.java`): -```java -@Component -public class SubmissionListener { - private final TestExecutionService testExecutionService; - private final ExecutionResultProducer executionResultProducer; - - @RabbitListener(queues = "${rabbitmq.queue:codehive_queue}") - public void handleExecutionJob(ExecutionJob job) { - // Execute all test cases and generate comprehensive report - ExecutionReport report = testExecutionService.executeJob(job); - - // Report contains: - // - Overall status (AC/WA/TLE/MLE/RTE/CE) - // - Individual test case results - // - Execution statistics - // - Stored in MinIO at job.getOutputPath() - - logger.info("Execution completed: id={}, status={}, passed={}/{}", - job.getId(), report.getOverallStatus(), - report.getPassedTests(), report.getTotalTests()); - - // Send result back to backend via RabbitMQ result queue - executionResultProducer.sendExecutionResult(report); - } -} -``` - -**Producer** (`messaging/producer/ExecutionResultProducer.java`): -```java -@Service -public class ExecutionResultProducer { - private final RabbitTemplate rabbitTemplate; - - public void sendExecutionResult(ExecutionReport report) { - logger.info("Sending execution result: executionId={}, status={}", - report.getExecutionId(), report.getOverallStatus()); - - rabbitTemplate.convertAndSend(RabbitMQConfig.RESULT_QUEUE_NAME, report); - } -} -``` - -**Test Execution Service** (`service/TestExecutionService.java`): -The service orchestrates test execution based on execution type: - -1. **DEFINITIVE Mode**: - - Downloads source code from `job.getSource()` - - For each test case (1 to `job.getNumTests()`): - - Downloads input from `{testsPath}/tc-{i}/tc-{i}.in` - - Downloads expected output from `{testsPath}/tc-{i}/tc-{i}.out` - - Executes student code with test input - - Compares output using `OutputComparatorService` - - Records result (AC/WA/TLE/MLE/RTE) - -2. **PRACTICE Mode**: - - Downloads student source from `job.getSource()` - - Downloads reference solution from `job.getReference()` - - For each inline test case in `job.getTestCases()`: - - Executes reference solution to get expected output - - Executes student code with same input - - Compares outputs using `OutputComparatorService` - - Records result (AC/WA/TLE/MLE/RTE) - -3. **Output Comparison** (`service/OutputComparatorService.java`): - - **EXACT_MATCH**: Trimmed line-by-line comparison - - **FLOATING_POINT**: Token-by-token with epsilon tolerance (1e-9) - -4. **Report Generation** (`model/dto/ExecutionReport.java`): - - Overall status (worst-case verdict) - - Per-test-case results with timing/memory - - Statistics: passed/failed counts, max time/memory - - Uploaded to MinIO as JSON at `job.getOutputPath()` - -**Configuration** (`config/RabbitMQConfig.java`): -```java -@Configuration -public class RabbitMQConfig { - public static final String QUEUE_NAME = System.getProperty("rabbitmq.queue", "codehive_queue"); - public static final String RESULT_QUEUE_NAME = System.getProperty("rabbitmq.result.queue", "codehive_result_queue"); - - @Bean - Queue executionQueue() { - return new Queue(QUEUE_NAME, true); // Durable queue - } - - @Bean - Queue resultQueue() { - return new Queue(RESULT_QUEUE_NAME, true); // Durable queue for results - } - - @Bean - public MessageConverter jsonMessageConverter() { - return new Jackson2JsonMessageConverter(); // JSON serialization - } -} -``` - -**MinIO Integration** (`service/ObjectStorageService.java`): -```java -@Service -public class ObjectStorageService { - private final MinioClient minioClient; - private final String bucketName = System.getProperty("minio.bucketName", "codehive"); - - public InputStream download(String objectKey) throws Exception { - return minioClient.getObject( - GetObjectArgs.builder() - .bucket(bucketName) - .object(objectKey) - .build() - ); - } - - public void upload(String objectKey, String content) throws Exception { - byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8); - ByteArrayInputStream stream = new ByteArrayInputStream(contentBytes); - - minioClient.putObject( - PutObjectArgs.builder() - .bucket(bucketName) - .object(objectKey) - .stream(stream, contentBytes.length, -1) - .contentType("text/plain") - .build() - ); - } -} -``` - -#### Sandbox Execution Flow - -1. **Message arrives** → `SubmissionListener` receives `ExecutionJob` from RabbitMQ queue (`codehive_queue`) -2. **Download files** → `ObjectStorageService` downloads source code and test input from MinIO -3. **Factory dispatch** → `LanguageExecutorFactory.getExecutor(language)` returns appropriate executor -4. **Temp file creation** → Executor creates temp directory and saves source/input files -5. **Compilation** (if needed) → Separate Docker container compiles code (Java, C, C++) - - Exit code != 0 → Return CE (Compilation Error) -6. **Container creation** → Executor creates isolated Docker container with: - - Language-specific image (openjdk, python, gcc) - - Memory limit (`--memory`, `--memory-swap`) - - CPU limit (`--cpu-quota`) - - Network isolation (`--network=none`) - - Volume mount for source/input files -7. **Code execution** → Run code with timeout monitoring via `Future.get(timeout)` - - Timeout → Return TLE (Time Limit Exceeded) - - Exit code 137 → Return MLE (Memory Limit Exceeded) - - Exit code != 0 → Return RTE (Runtime Error) - - Exit code 0 → Return AC (Accepted) or WA (Wrong Answer, TODO) -8. **Result collection** → Capture stdout, stderr, exit code, execution time -9. **Cleanup** → Remove Docker container, delete temp files -10. **Result publishing** → `ExecutionResultProducer` sends `ExecutionReport` to `codehive_result_queue` -11. **Backend processing** → `ExecutionResultListener` receives report, `ExecutionResultService` updates `Execution` entity - -**Docker Images**: -- Java: `openjdk:21-slim` (compilation: `javac`, execution: `java`) -- Python: `python:3.11-slim` (execution: `python`, syntax errors detected as CE) -- C: `gcc:latest` (compilation: `gcc -o program main.c -lm`, execution: `./program`) -- C++: `gcc:latest` (compilation: `g++ -o program main.cpp -std=c++17 -lm`, execution: `./program`) - -**Resource Limits**: -- Default time limit: 5000ms (5 seconds) -- Default memory limit: 256MB -- CPU limit: 1 CPU (100000 quota) -- Network: Disabled (`--network=none`) -- Compilation memory: 512MB (fixed) - -### Frontend TypeScript/React Patterns - -#### Routing -- **React Router v7** with file-based routing in `app/routes/` -- Routes defined in `app/routes.ts` -- Pattern: `route("path", "routes/filename.tsx")` - -#### Service Layer -```typescript -// Pattern: {Domain}Service class with methods returning Promises -// Example: services/AuthService.ts -class AuthServiceClass { - private readonly baseUrl = `${API_BASE_URL}/api/auth`; - - async login(credentials: LoginRequest): Promise> { - const response = await fetch(`${this.baseUrl}/login`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(credentials), - }); - - const data = await response.json(); - if (!response.ok) { - throw new Error(data.message || "Login failed"); - } - return data; - } - - // Token management methods - setToken(token: string): void { localStorage.setItem("authToken", token); } - getToken(): string | null { return localStorage.getItem("authToken"); } - removeToken(): void { localStorage.removeItem("authToken"); } -} - -export const AuthService = new AuthServiceClass(); -``` - -#### Type Definitions -- **Request types**: `types/request/Auth.ts` -- **Response types**: `types/response/Api.ts` -- **Model types**: `types/model/User.ts` -- Mirror backend DTOs exactly - -#### Component Organization -- **Pages**: `pages/{PageName}Page.tsx` (composed of components) -- **Components**: `components/{ComponentName}.tsx` (reusable) -- **Routes**: `routes/{route-name}.tsx` (route handlers that render pages) - -#### Styling -- **Tailwind CSS v4** via `@tailwindcss/vite` -- Dark mode support via `ThemeContext` -- Utility-first classes - ---- - -## Security & Authentication - -### JWT Authentication Flow - -1. **Login/Signup**: User submits credentials → Backend validates → Returns JWT token + user data -2. **Token Storage**: Frontend stores token in `localStorage` via `AuthService.setToken()` -3. **Protected Requests**: Frontend sends `Authorization: Bearer ` header -4. **Backend Validation**: `JWTAuthenticationFilter` intercepts, validates token, sets `SecurityContextHolder` - -### JWT Configuration -- **Secret**: Stored in `.env` as `JWT_SECRET` (base64-encoded, min 256 bits) -- **Expiration**: `JWT_EXPIRATION_MS` (default: 86400000ms = 24 hours) -- **Claims**: `sub` (user email), `scope` (user scopes), `iat`/`exp` timestamps - -### Security Configuration -- **CSRF**: Disabled (stateless JWT authentication) -- **CORS**: Configured for `FRONTEND_URL` from `.env` -- **Sessions**: Stateless (`.sessionManagement().sessionCreationPolicy(STATELESS)`) -- **Password Encoding**: BCrypt with strength 10 - -### Rate Limiting -- **Login**: 5 requests per 60 seconds per IP -- **Signup**: 3 requests per 5 minutes per IP -- **Forgot Password**: 3 requests per 5 minutes per IP -- **Reset Password**: 5 requests per 5 minutes per IP - ---- - -## Environment Variables +Typical local flow: +1. Start infrastructure (PostgreSQL, RabbitMQ, and other required services) with Docker Compose from backend. +2. Run backend from codehive-backend. +3. Run worker from codehive-worker when execution pipelines are needed. +4. Run frontend from codehive-frontend. -### Backend `.env` (codehive-backend/) -```bash -# Database -DATABASE_NAME=codehive -DATABASE_USERNAME=myuser -DATABASE_PASSWORD=secret +Use project wrappers and local scripts: +- Backend and worker: ./gradlew +- Frontend: npm scripts in package.json -# JWT -JWT_SECRET= -JWT_EXPIRATION_MS=86400000 +## Testing +Run tests in each project independently: +- Backend: unit and integration tests via Gradle +- Worker: service and sandbox-related tests via Gradle +- Frontend: type checks and UI/app tests via npm scripts -# Mail (SMTP) -MAIL_HOST=smtp.gmail.com -MAIL_PORT=587 -MAIL_USERNAME=your-email@gmail.com -MAIL_PASSWORD=your-app-password -MAIL_SMTP_AUTH=true -MAIL_STARTTLS_ENABLE=true - -# Frontend URL (for CORS) -FRONTEND_URL=http://localhost:5173 -``` - -### Frontend `.env` (codehive-frontend/) -```bash -VITE_API_URL=http://localhost:8080 -``` - ---- - -## Database - -### Technology -- **Production**: PostgreSQL 18.1-alpine -- **Testing**: H2 in-memory database (configured in `application-test.properties`) - -### Entities - -**User** (`model/entity/User.java`): -- `id` (Long, auto-generated) -- `name`, `lastName`, `enrollmentNumber` (unique), `email` (unique) -- `password` (BCrypt-encoded) -- `role` (enum: STUDENT, TEACHER, ADMIN) -- `scopes` (List, many-to-many in `user_scopes` table) -- `profilePictureUrl`, `isActive`, `createdAt` -- Implements `UserDetails` for Spring Security - -**PasswordResetToken** (`model/entity/PasswordResetToken.java`): -- `id`, `token` (UUID), `user` (ManyToOne), `expiryDate`, `used` - -### Migration Strategy -- Currently: JPA auto-DDL (Hibernate `ddl-auto=update`) -- **TODO**: Migrate to Flyway/Liquibase for production - ---- - -## API Endpoints - -### Authentication (`/api/auth`) - -#### `POST /api/auth/login` -**Rate Limit**: 5 req/60s -**Request**: -```json -{ - "identifier": "user@example.com or ENR001", - "password": "password123" -} -``` -**Response** (200): -```json -{ - "success": true, - "message": "Login successful", - "data": { - "token": "eyJhbGciOiJIUzI1...", - "user": { - "id": 1, - "name": "John", - "lastName": "Doe", - "email": "user@example.com", - "enrollmentNumber": "ENR001", - "role": "STUDENT", - "scopes": ["READ", "WRITE"], - "profilePictureUrl": "...", - "isActive": true, - "createdAt": "2024-01-15T10:00:00" - } - } -} -``` - -#### `POST /api/auth/signup` -**Rate Limit**: 3 req/5min -**Request**: -```json -{ - "name": "Jane", - "lastName": "Doe", - "email": "jane@example.com", - "enrollmentNumber": "ENR002", - "password": "password123", - "role": "STUDENT" -} -``` -**Response** (201): Same as login response - -### Password Recovery (`/api/recovery-password`) - -#### `POST /api/recovery-password/forgot` -**Rate Limit**: 3 req/5min -**Request**: -```json -{ - "email": "user@example.com" -} -``` -**Response** (200): -```json -{ - "success": true, - "message": "Password reset email sent" -} -``` - -#### `POST /api/recovery-password/reset` -**Rate Limit**: 5 req/5min -**Request**: -```json -{ - "token": "uuid-token-from-email", - "newPassword": "newpassword123" -} -``` -**Response** (200): -```json -{ - "success": true, - "message": "Password reset successful" -} -``` - ---- +Testing rules: +- Add or update tests for every behavior change. +- Keep tests isolated and deterministic. +- Prefer small unit tests plus focused integration coverage. ## CI/CD - -### GitHub Actions Workflow (`.github/workflows/backend-ci.yml`) - -**Triggers**: -- Push to `main` or `develop` branches (backend changes only) -- Pull requests to `main` or `develop` - -**Pipeline Steps**: -1. **Setup**: Java 21 (Temurin), Gradle caching -2. **Test**: `./gradlew test --info` with `test` profile (H2 database) -3. **Coverage**: `./gradlew jacocoTestReport` -4. **Build**: `./gradlew build -x test` (creates JAR) -5. **Artifacts**: Upload test results, coverage reports, JAR -6. **PR Checks**: Publish test results, add coverage comment (min 70% overall, 80% changed files) - -**Requirements for Passing**: -- All tests pass (121+ tests) -- No build errors -- Coverage thresholds met - ---- - -## Testing Strategy - -### Test Pyramid -- **70% Unit Tests**: Fast, isolated, mock dependencies -- **30% Integration Tests**: Slower, full Spring context, real database - -### Coverage Targets -- **Overall**: 70%+ (enforced in CI) -- **Changed Files**: 80%+ (enforced in PR checks) -- **Current**: 70%+ (121 tests) - -### Writing Tests - -**For new features, always add**: -1. **Unit tests** for service layer logic -2. **Integration tests** for controller endpoints -3. **Edge cases**: null inputs, invalid data, error paths -4. **Security tests**: unauthorized access, rate limiting - -**Test Naming**: -- Method: `methodName_Scenario_ExpectedOutcome` -- Display: `@DisplayName("Human-readable description")` - -**Example**: -```java -@Test -@DisplayName("Throws IncorrectCredentialsException when password is wrong") -void login_WithWrongPassword_ThrowsIncorrectCredentialsException() { - // Given - LoginRequest request = new LoginRequest("user@example.com", "wrongpass"); - when(userRepository.findByEmail("user@example.com")).thenReturn(Optional.of(testUser)); - when(passwordEncoder.matches("wrongpass", testUser.getPassword())).thenReturn(false); - - // When/Then - assertThatThrownBy(() -> authService.login(request)) - .isInstanceOf(IncorrectCredentialsException.class) - .hasMessage("Invalid credentials"); -} -``` - ---- - -## Git Workflow - -### Branch Naming -- **Feature**: `feature/{description}` or `feature/{jira-task-id}` -- **Bugfix**: `bugfix/{description}` -- **Hotfix**: `hotfix/{description}` - -### Commit Messages -- Use conventional commits (optional but recommended) -- Examples: `feat: add user login`, `fix: resolve JWT expiration bug`, `test: add integration tests for signup` - -### Pull Requests -- Use `PR_template.md` checklist -- **Required checks**: - - [ ] All tests pass - - [ ] Coverage thresholds met - - [ ] Code follows style guidelines - - [ ] Self-review completed - - [ ] Documentation updated (if needed) - -### Current Branch -- Active development on `feature/rabbitMQ` (integrating RabbitMQ for worker communication) - ---- - -## Common Tasks - -### Adding a New Language Executor to Worker - -1. **Create Executor Class** in `sandbox/{language}/{Language}Executor.java`: - ```java - @Component("languagename") // Bean name must match language identifier - public class LanguageExecutor implements LanguageExecutor { - // Implement execution logic - } - ``` - -2. **Implement Execution Logic**: - - Create Docker container with appropriate image (e.g., `openjdk:21`, `python:3.11`, `gcc:latest`) - - Mount code as volume or write to container filesystem - - Set resource limits (CPU, memory, network: none) - - Execute with timeout (use Resilience4j TimeLimiter) - - Capture stdout, stderr, exit code - - Clean up container (`docker rm -f`) - -3. **Add Dependencies** (if needed): - - Update `build.gradle.kts` with language-specific libraries - - Add Docker image pull logic in executor constructor - -4. **Test Execution**: - - Create test class in `src/test/java/` - - Use Testcontainers to verify Docker execution - - Test edge cases: infinite loops, memory leaks, compilation errors - -5. **Factory Auto-Discovery**: - - No changes needed - `LanguageExecutorFactory` auto-discovers by bean name - - Verify: `factory.getExecutor("languagename")` returns your executor - -### Adding a New API Endpoint - -1. **Define Request DTO** in `model/request/{domain}/{Action}Request.java`: - - Add Jakarta validation annotations - - No-arg + parameterized constructors - - Getters/setters - -2. **Define Response DTO** (if needed) in `model/response/{domain}/{Action}Response.java`: - - Extend `ApiResponse` or use `SuccessResponse` - - Add `@JsonInclude(JsonInclude.Include.NON_NULL)` - -3. **Create Custom Exceptions** in `model/exception/{domain}/{Error}Exception.java`: - - Extend `RuntimeException` - - Add 4 constructors - -4. **Update GlobalExceptionHandler** in `model/exception/handler/GlobalExceptionHandler.java`: - - Add `@ExceptionHandler` method for new exception - - Return `ResponseEntity` with appropriate HTTP status - -5. **Implement Service Logic** in `service/{Domain}Service.java`: - - `@Service` annotation - - Constructor injection of dependencies - - `@Transactional` on methods - - Throw custom exceptions on errors - -6. **Create Controller Endpoint** in `controller/{Domain}Controller.java`: - - `@RestController` + `@RequestMapping("/api/{domain}")` - - OpenAPI annotations (`@Operation`, `@ApiResponses`) - - `@RateLimit` if needed - - Call service method, return `ResponseEntity>` - -7. **Write Tests**: - - Unit tests for service (`{Domain}ServiceTest.java`) - - Integration tests for controller (`{Domain}ControllerIntegrationTest.java`) - - Aim for 70%+ coverage - -8. **Update API Documentation**: - - OpenAPI annotations auto-generate Swagger docs - - Test at http://localhost:8080/swagger-ui.html - -### Adding a New Entity - -1. **Create Entity** in `model/entity/{Entity}.java`: - - `@Entity` + `@Table(name = "...")` - - JPA annotations on fields - - No-arg constructor + builder pattern (optional) - -2. **Create Repository** in `repository/{Entity}Repository.java`: - - Extend `JpaRepository` - - Add custom query methods (Spring Data JPA naming) - -3. **Create DTO** in `model/dto/{Entity}DTO.java`: - - Fields for external representation - - `@JsonInclude(JsonInclude.Include.NON_NULL)` - -4. **Create Mapper** in `model/mapper/{Entity}Mapper.java`: - - Static methods: `toDTO(Entity)`, `toEntity(DTO)` - -5. **Update Database**: - - Currently auto-DDL, no manual migration needed - - For production, add Flyway/Liquibase migration - -### Adding Frontend Page - -1. **Create Route Handler** in `app/routes/{route-name}.tsx`: - - Import page component - - Export default component function - -2. **Register Route** in `app/routes.ts`: - - Add `route("path", "routes/{route-name}.tsx")` - -3. **Create Page Component** in `app/pages/{PageName}Page.tsx`: - - Use existing components from `app/components/` - - Call service methods from `app/services/` - -4. **Define Types** (if needed): - - Request types in `app/types/request/` - - Response types in `app/types/response/` - - Model types in `app/types/model/` - -5. **Add Service Method** in `app/services/{Domain}Service.ts`: - - Follow existing pattern with `fetch` API - - Handle errors with try/catch - - Return typed promises - ---- - -## Gotchas & Important Notes - -### Backend - -1. **Identifier in Login**: The `LoginRequest.identifier` field accepts **both** email and enrollment number. `AuthService.isEmail()` determines which to use. - -2. **Rate Limiting by IP**: Rate limits are enforced per IP address (extracted from `X-Forwarded-For` or `RemoteAddr`). In development behind a proxy, all requests may share the same IP. - -3. **Password Encoding**: Always use `passwordEncoder.encode()` before saving passwords. Never store plain text. - -4. **Transaction Management**: Use `@Transactional` on service methods that modify data. Use `readOnly = true` for read-only operations (performance optimization). - -5. **H2 Console in Tests**: Test profile uses H2 in-memory database. Data is reset between tests due to `@Transactional` on test classes. - -6. **JWT Secret**: Must be at least 256 bits (32 bytes) base64-encoded. Generate with: `openssl rand -base64 32` - -7. **Gradle Wrapper**: Always use `./gradlew` (Unix) or `gradlew.bat` (Windows), not system-installed Gradle, to ensure correct version. - -8. **Docker Compose Integration**: Spring Boot DevTools auto-detects `docker-compose.yaml` and starts containers. Can be disabled by removing `spring-boot-docker-compose` dependency. - -9. **Scope System**: Currently defined but not fully implemented. `User.scopes` is a placeholder for future fine-grained permissions. - -10. **OpenAPI Documentation**: Always add `@Operation` and `@ApiResponses` to controller methods. Swagger UI auto-updates on application restart. - -### Worker - -1. **Docker Daemon Required**: Worker service requires Docker daemon running on the host. Ensure Docker socket is accessible (`/var/run/docker.sock` on Unix). - -2. **Language Bean Names**: Executor bean names (`@Component("java")`) **must** match the language identifiers used in `SubmissionMessage`. Case-sensitive. - -3. **Container Cleanup**: Always clean up Docker containers after execution, even on errors. Use try-finally or `@PreDestroy` hooks. - -4. **Execution Timeouts**: Implement timeouts using Resilience4j `TimeLimiter` (already in dependencies). Default should be 10-30 seconds per execution. - -5. **Resource Limits**: Set Docker container resource limits: `--memory=512m`, `--cpus=1.0`, `--network=none` for security. - -6. **Image Availability**: Worker assumes Docker images are pre-pulled. Add image pull logic in executor constructors or startup to avoid delays. - -7. **RabbitMQ Connection**: Worker connects to RabbitMQ on localhost:5672 by default. Configure via `spring.rabbitmq.*` properties if different. - -8. **Message Acknowledgement**: Use manual acknowledgement for RabbitMQ messages. Only ack after successful execution and result publishing. - -9. **Testcontainers**: Worker tests use Testcontainers (see `TestcontainersConfiguration.java`). Requires Docker for running tests. - -10. **Security Isolation**: Never trust user code. Containers must run with `--network=none`, read-only filesystem where possible, and no privileged access. - -### Frontend - -1. **API URL Configuration**: Set `VITE_API_URL` in `.env` for non-localhost backends. Defaults to `http://localhost:8080`. - -2. **React Router v7**: Uses file-based routing. Routes must be registered in `routes.ts`, not auto-discovered. - -3. **SSR Enabled**: Server-side rendering is enabled by default (`ssr: true` in `react-router.config.ts`). Can be disabled for pure SPA mode. - -4. **Token Expiration**: Frontend doesn't automatically handle token expiration. Backend returns 401, but frontend needs to implement redirect to login. - -5. **Dark Mode**: Managed by `ThemeContext`. Use `useTheme()` hook to access `theme` and `toggleTheme()`. - -6. **Tailwind CSS v4**: Latest version with different configuration. Check official docs if migrating from v3. - -### Testing - -1. **Test Isolation**: Integration tests use `@Transactional` to rollback after each test. Don't rely on database state between tests. - -2. **Mocking vs Real Beans**: Unit tests mock dependencies. Integration tests use real Spring beans (except external services like SMTP). - -3. **Coverage Reports**: Generated in `build/reports/jacoco/test/html/index.html` after `./gradlew jacocoTestReport`. - -4. **Flaky Tests**: Rate limiting tests may be flaky if system clock skews. Use `@DirtiesContext` if needed. - -### Infrastructure - -1. **RabbitMQ Integration**: ✅ **FULLY IMPLEMENTED** - Bidirectional communication between backend and worker: - - Backend sends `ExecutionJob` to `codehive_queue` via `ExecutionProducer` - - Worker processes jobs and sends `ExecutionReport` to `codehive_result_queue` via `ExecutionResultProducer` - - Backend receives results via `ExecutionResultListener` and updates `Execution` entity via `ExecutionResultService` - -2. **MinIO Integration**: ✅ **IMPLEMENTED** - Backend and worker use MinIO for object storage. Source code and test inputs are stored as files in MinIO bucket (`codehive`), referenced by object keys in `ExecutionJob`. - -3. **Docker Networking**: Backend, worker, PostgreSQL, RabbitMQ, and MinIO communicate via Docker Compose network. Worker accesses host Docker daemon via socket mount (`/var/run/docker.sock`) to create isolated execution containers. - -4. **Port Conflicts**: Ensure ports are available: - - 5432: PostgreSQL - - 5672/15672: RabbitMQ (AMQP/Management UI) - - 9000/9001: MinIO (API/Console) - - 8080: Backend API - - 3000/5173: Frontend dev server - -5. **Worker Deployment**: In production, worker should run on separate machines with Docker installed. Use Docker-in-Docker or bind mount Docker socket with caution (security implications). Ensure MinIO and RabbitMQ are accessible from worker. - -6. **Language Executors**: ✅ **IMPLEMENTED** - All four language executors (Java, Python, C, C++) are fully implemented with: - - Docker-based sandboxing - - Resource limits (memory, CPU, network isolation) - - Timeout detection (TLE) - - Memory limit detection (MLE) - - Compilation error detection (CE) - - Runtime error detection (RTE) - - Output comparison for AC/WA verification (EXACT_MATCH and FLOATING_POINT modes) - -7. **Test Execution Pipeline**: ✅ **IMPLEMENTED** - Complete orchestration of test case execution: - - DEFINITIVE mode: Batch execution against stored test cases from MinIO - - PRACTICE mode: Dynamic execution against inline test cases with reference solution comparison - - Per-test-case result tracking with detailed feedback - - Comprehensive execution reports with statistics (passed/failed, timing, memory) - - JSON report upload to MinIO for backend consumption - -8. **Result Publishing Pipeline**: ✅ **IMPLEMENTED** - Complete result flow from worker to backend: - - Worker's `ExecutionResultProducer` publishes `ExecutionReport` to `codehive_result_queue` - - Backend's `ExecutionResultListener` consumes results from the queue - - `ExecutionResultService` updates `Execution` entity with status, timing, and memory stats - - Handles error cases by sending error reports back to backend - ---- - -## Troubleshooting - -### Backend Won't Start - -**Issue**: `Connection refused` to PostgreSQL -**Solution**: Ensure Docker containers are running: `docker-compose up -d` - -**Issue**: `Invalid JWT_SECRET` error -**Solution**: Check `.env` has valid base64-encoded secret (min 256 bits) - -**Issue**: Tests fail with database errors -**Solution**: Check `application-test.properties` is configured for H2, not PostgreSQL - -### Worker Won't Start - -**Issue**: `Cannot connect to Docker daemon` -**Solution**: Ensure Docker daemon is running: `docker info` or `systemctl start docker` - -**Issue**: `Connection refused` to RabbitMQ -**Solution**: Start RabbitMQ via backend's `docker-compose up -d rabbitmq` - -**Issue**: Container creation fails -**Solution**: Pull required images manually: `docker pull openjdk:21-slim`, `docker pull python:3.11-slim`, `docker pull gcc:latest` - -**Issue**: Tests fail with Testcontainers errors -**Solution**: Ensure Docker is accessible for tests. Check Docker socket permissions. - -**Issue**: MinIO connection errors -**Solution**: Ensure MinIO is running and accessible. Check `minio.url`, `minio.accessKey`, `minio.secretKey` in environment variables or system properties. - -**Issue**: Execution jobs not being consumed -**Solution**: Check RabbitMQ queue name matches between backend and worker (`codehive_queue`). Verify worker is connected to RabbitMQ via management UI (http://localhost:15672). - -**Issue**: Language executor not found -**Solution**: Verify bean names match enum values: `@Component("JAVA")`, `@Component("PYTHON")`, `@Component("C")`, `@Component("CPP")` (all uppercase). - -### Frontend Can't Connect to Backend - -**Issue**: CORS errors in browser console -**Solution**: Check `FRONTEND_URL` in backend `.env` matches frontend dev server URL - -**Issue**: 404 on API calls -**Solution**: Verify `VITE_API_URL` in frontend `.env` is correct - -### Tests Failing - -**Issue**: Rate limit tests fail intermittently -**Solution**: Add `Thread.sleep()` between requests or use `@DirtiesContext` - -**Issue**: Integration tests fail with "table not found" -**Solution**: Ensure `@ActiveProfiles("test")` is present on test class - -### Coverage Below Threshold - -**Issue**: CI fails with "Coverage below 70%" -**Solution**: Add more unit tests for uncovered service methods. Focus on edge cases and error paths. - ---- - -## Additional Resources - -- **Spring Boot Docs**: https://spring.io/projects/spring-boot -- **Spring AMQP (RabbitMQ)**: https://spring.io/projects/spring-amqp -- **Docker Java Client**: https://github.com/docker-java/docker-java -- **MinIO Java SDK**: https://min.io/docs/minio/linux/developers/java/minio-java.html -- **React Router v7 Docs**: https://reactrouter.com/ -- **Bucket4j (Rate Limiting)**: https://bucket4j.com/ -- **Jacoco (Coverage)**: https://www.jacoco.org/jacoco/ -- **Testcontainers**: https://testcontainers.com/ -- **RabbitMQ Docs**: https://www.rabbitmq.com/documentation.html - ---- - -## Future Enhancements - -1. ~~**Result Publishing**~~: ✅ **COMPLETED** - Bidirectional RabbitMQ communication implemented -2. **Real-time Collaboration**: WebSocket support for live code editing -3. **Group Management**: Teacher/student group creation and assignment submission -4. **File Upload**: Support for uploading code files and project archives -5. **Flyway Migrations**: Replace JPA auto-DDL with versioned migrations -6. **Refresh Tokens**: Implement refresh token rotation for longer sessions -7. **Email Templates**: HTML email templates for password reset and notifications -8. **Admin Dashboard**: Frontend admin panel for user management -9. **API Versioning**: Add `/v1/` prefix to API routes for future compatibility -10. **Monitoring**: Add Actuator endpoints and Prometheus metrics for worker execution metrics -11. **Multi-file Support**: Worker support for projects with multiple source files and complex build systems -12. **Language Extensions**: Add support for Go, Rust, JavaScript, TypeScript, Ruby -13. **Execution History**: Store and display past code execution results with analytics -14. **Security Hardening**: Add seccomp profiles, AppArmor/SELinux policies for worker containers -15. **Horizontal Scaling**: Support multiple worker instances with load balancing - ---- - -**Last Updated**: 2026-01-22 -**Current Branch**: `feature/rabbitMQ` -**Project Status**: Active Development (Alpha) -**Worker Status**: ✅ **FULLY OPERATIONAL** - Complete bidirectional communication pipeline: -- RabbitMQ integration with comprehensive `ExecutionJob` model -- MinIO integration for source code, test cases, and execution reports -- All 4 language executors (Java, Python, C, C++) with Docker sandboxing -- DEFINITIVE mode: Multi-test execution with stored test cases -- PRACTICE mode: Reference solution comparison -- Output comparison service with EXACT_MATCH and FLOATING_POINT modes -- Comprehensive execution reports with per-test-case results and statistics -- TLE/MLE/RTE/CE/AC/WA verdict detection -- Report storage in MinIO as JSON -- ✅ Result publishing to backend via `codehive_result_queue` -- ✅ Backend listener updates `Execution` entity with results +CI is managed with GitHub Actions under .github/workflows/. + +Expected checks for changes: +- Build passes +- Relevant tests pass +- Coverage gates (when configured) are met +- No broken formatting or lint checks + +Before opening PRs, run local checks for the affected project(s). + +## Linting And Code Style +Apply style tools per project: +- Java: follow existing formatter and conventions in backend/worker +- Frontend: follow existing TypeScript, React, and styling conventions + +Rules: +- Keep naming and package/module structure consistent with existing code. +- Avoid unrelated refactors in feature/fix PRs. +- Keep diffs focused and minimal. + +## Architecture +High-level architecture: +- Frontend consumes backend REST APIs. +- Backend handles auth, core business logic, persistence, and messaging orchestration. +- Worker consumes execution jobs, runs sandboxed code, and publishes execution results. +- Shared infrastructure includes RabbitMQ and object storage. + +For concrete architecture, contracts, data models, and execution flow, always consult: +- llms/backend/OVERVIEW.md +- llms/frontend/OVERVIEW.md +- llms/worker/OVERVIEW.md + +Then drill into corresponding llms subfolders for implementation details. + +## Documentation Routing Rule +When working on a specific area, read docs in this order: +1. Relevant llms//OVERVIEW.md +2. Relevant llms/// documentation +3. Source code in the target module + +Do not place deep implementation playbooks in this root AGENTS.md. +Keep this file focused on repository-wide standards only. diff --git a/llms/backend/OVERVIEW.md b/llms/backend/OVERVIEW.md new file mode 100644 index 0000000..109d8ee --- /dev/null +++ b/llms/backend/OVERVIEW.md @@ -0,0 +1,80 @@ +# Backend Overview + +## What This Component Does +The backend is the central API for CodeHive. It handles authentication, user flows, execution orchestration, persistence, and integration with infrastructure services such as PostgreSQL, RabbitMQ, and object storage. + +Main responsibilities: +- Expose REST endpoints used by the frontend. +- Validate and authorize requests. +- Execute business logic in service classes. +- Persist and query data with JPA repositories. +- Publish and consume execution-related messages. +- Return standardized API responses and errors. + +## How It Works +Typical request flow: +1. A client calls a controller endpoint. +2. Request payload is validated. +3. Security and rate-limiting checks are applied. +4. Controller delegates to a service. +5. Service uses repositories, utilities, messaging, or external integrations. +6. A structured response is returned to the client. + +Execution flow at a high level: +1. Backend receives an execution request. +2. Backend stores required metadata and/or files. +3. Backend publishes a message to RabbitMQ for the worker. +4. Worker processes the job and publishes results. +5. Backend consumes the result and exposes it through API endpoints. + +## Useful Commands +Run these from codehive-backend. + +Setup and run: +- ./gradlew bootRun +- ./gradlew build +- ./gradlew clean build + +Tests and coverage: +- ./gradlew test +- ./gradlew jacocoTestReport + +Infrastructure services (from codehive-backend): +- docker compose up -d +- docker compose down + +Common local endpoints: +- API: http://localhost:8080 +- Swagger UI: http://localhost:8080/swagger-ui.html + +## Project Folder Structure +Runtime module structure (codehive-backend/src/main/java/com/github/codehive): +- config: Spring and infrastructure configuration. +- controller: HTTP entry points. +- service: Business logic and orchestration. +- repository: Data access layer. +- model: Entities, DTOs, requests, responses, exceptions. +- security: Auth and security-related classes. +- messaging: Queue listeners/producers and messaging contracts. +- ratelimit: Request throttling concerns. +- websocket: Realtime communication support. +- utils: Shared utility helpers. + +Testing structure: +- codehive-backend/src/test/java for unit and integration tests. + +Backend docs structure in llms/backend: +- OVERVIEW.md: This document. +- auth: Authentication and authorization details. +- controller: Controller-level conventions and API patterns. +- executions: Execution request and result lifecycle. +- messaging: Queue contracts, listeners, and producers. +- model: Data model and DTO conventions. +- security: Security architecture and policies. +- service: Service-layer behavior and orchestration rules. + +## How To Navigate Backend Docs +Read in this order: +1. llms/backend/OVERVIEW.md +2. The relevant domain folder in llms/backend +3. The corresponding source package in codehive-backend diff --git a/llms/backend/auth/README.md b/llms/backend/auth/README.md new file mode 100644 index 0000000..9af84de --- /dev/null +++ b/llms/backend/auth/README.md @@ -0,0 +1,73 @@ +# Backend Auth Implementation + +## Scope +This document explains the authentication and user registration behavior implemented in codehive-backend. + +## Main Entry Points +- POST /api/auth/login +- POST /api/auth/signup +- POST /api/auth/signup/csv +- GET /api/auth/me + +Controller: codehive-backend/src/main/java/com/github/codehive/controller/AuthController.java + +## Login Flow +1. Request is validated through LoginRequest. +2. Identifier is interpreted as email or enrollment number. +3. User is loaded from UserRepository. +4. Password is verified with PasswordEncoder. +5. JWT is generated with claims (userId, role) and subject email. +6. Response returns AuthResponse inside SuccessResponse. + +Service implementation: codehive-backend/src/main/java/com/github/codehive/service/AuthService.java + +## Registration Flow (Admin Only) +1. Endpoint is protected with @PreAuthorize("hasAuthority('ADMIN')"). +2. Request is validated through SignUpRequest. +3. Service checks duplicate email and enrollment number. +4. Temporary password is generated and encoded. +5. User is created with temporaryPassword=true and isActive=true. +6. Welcome email is sent with temporary credentials. + +## Bulk Registration Flow (CSV) +1. Admin uploads CSV to /api/auth/signup/csv. +2. Controller validates file and creates taskId. +3. CsvRegistrationService.processAsync parses and validates rows. +4. Progress is streamed through WebSocket channel /ws/csv-progress. +5. Each valid row creates a user and sends welcome email. + +Important validation rules: +- Exactly 6 columns expected per row. +- Role must be STUDENT, TEACHER, or ADMIN. +- Required fields cannot be empty. +- Email format must be valid. +- Duplicate email/enrollment is blocked both in-file and in-database. + +## Current User (/me) +1. Controller reads Authorization header. +2. Bearer token is extracted. +3. AuthService resolves user from token subject. +4. UserDTO is returned. + +## Data and Contracts +Request classes: +- model/request/auth/LoginRequest.java +- model/request/auth/SignUpRequest.java + +Response classes: +- model/response/auth/AuthResponse.java +- model/response/SuccessResponse.java + +User storage model: +- model/entity/User.java + +## Error and Security Behavior +- Invalid credentials throw IncorrectCredentialsException. +- Duplicate email/enrollment throws conflict exceptions. +- Validation errors are handled by GlobalExceptionHandler. +- Endpoint rate limits are enforced via @RateLimit. + +## Notes for Future Changes +- If role/permission model expands, update JWT claims and authorities mapping. +- Keep LoginRequest backward compatible because frontend and clients depend on identifier semantics. +- Bulk CSV should remain async to avoid request timeouts. diff --git a/llms/backend/controller/README.md b/llms/backend/controller/README.md new file mode 100644 index 0000000..cc4e3ad --- /dev/null +++ b/llms/backend/controller/README.md @@ -0,0 +1,56 @@ +# Backend Controller Layer + +## Scope +This document describes how HTTP controllers are implemented and how requests flow to services. + +Controller classes: +- codehive-backend/src/main/java/com/github/codehive/controller/AuthController.java +- codehive-backend/src/main/java/com/github/codehive/controller/RecoveryPasswordController.java +- codehive-backend/src/main/java/com/github/codehive/controller/CheckExecutionController.java + +## Controller Responsibilities +- Define API routes and HTTP semantics. +- Validate request payloads with @Valid. +- Apply endpoint-level limits via @RateLimit. +- Enforce authorization with @PreAuthorize where needed. +- Return a consistent wrapper response (SuccessResponse/ErrorResponse). +- Delegate business logic to service classes. + +## Response Pattern +Success responses typically use: +- SuccessResponse for data payloads. + +Error handling is centralized in: +- model/exception/handler/GlobalExceptionHandler.java + +## Endpoint Group Behavior +AuthController: +- Login, signup, current user, CSV bulk signup. +- Signup routes require admin authority. + +RecoveryPasswordController: +- Forgot password and reset password flows. +- Returns generic success messages to avoid account enumeration. + +CheckExecutionController: +- POST /api/execution/check creates execution and queues worker job. +- GET /api/execution/check/{id} polls execution status. + +## Cross-Cutting Concerns +- OpenAPI annotations are used for API docs. +- Rate limiting is applied through custom annotation and aspect. +- Security is primarily in filters/config, not in controllers. + +## Implementation Conventions +- Keep controllers thin and orchestration-only. +- Do not embed repository logic in controllers. +- Prefer explicit HTTP status codes (200, 201, 202, etc). +- Keep route grouping by domain under /api/{domain}. + +## Adding a New Controller +1. Create class under controller package. +2. Define request/response DTOs under model/request and model/response. +3. Add service method and keep business logic there. +4. Add @RateLimit if endpoint can be abused. +5. Add method security annotations when role-restricted. +6. Document endpoint with OpenAPI annotations. diff --git a/llms/backend/executions/README.md b/llms/backend/executions/README.md new file mode 100644 index 0000000..af2d2fe --- /dev/null +++ b/llms/backend/executions/README.md @@ -0,0 +1,75 @@ +# Backend Execution Orchestration + +## Scope +This document explains how the backend receives execution requests, persists execution state, stores source code, and coordinates with the worker. + +Primary classes: +- controller/CheckExecutionController.java +- service/ExecutionRequestService.java +- service/ExecutionResultService.java +- messaging/producer/ExecutionRequestProducer.java +- messaging/listener/ExecutionResultListener.java + +## Request-to-Queue Flow +1. Client sends ExecutionRequest to POST /api/execution/check. +2. Controller validates payload and delegates to ExecutionRequestService. +3. Service creates Execution entity with status PENDING. +4. Source code is uploaded to MinIO via ObjectStorageService. +5. Service builds ExecutionJob payload. +6. Producer sends ExecutionJob to RabbitMQ queue. +7. API returns 202 Accepted with ExecutionDTO (for polling). + +## Result Processing Flow +1. Worker publishes ExecutionReport. +2. ExecutionResultListener consumes the report. +3. ExecutionResultService loads execution by id. +4. Status/time/memory are updated in the executions table. +5. Client retrieves updated status via GET /api/execution/check/{id}. + +## Key Data Contracts +API request: +- model/request/execution/ExecutionRequest.java + +Queue request: +- model/dto/queue/ExecutionJob.java + +Queue result: +- model/dto/queue/ExecutionReport.java + +API polling response: +- model/dto/ExecutionDTO.java + +## Current Constraints and Defaults +- Time and memory limits are currently static defaults in ExecutionRequestService: + - time: 1000 ms + - memory: 256 MB +- Comparator type is currently fixed to EXACT_MATCH. +- Reference language is currently hard-coded in job building path. +- TODO comments indicate this is temporary and should become assignment-driven. + +## Storage Keys and Artifacts +Object paths are generated through ObjectKeyBuilder: +- execution source keys +- execution output keys +- test suite and reference keys + +File extension mapping is resolved by FileExtensionUtil based on Language enum. + +## Persistence Model +Execution entity fields track: +- executionType +- status +- timeMs +- memoryMb +- createdAt +- isOutdated +- optional user +- optional submission + +Repository: +- repository/ExecutionRepository.java + +## Extension Guidance +- Move defaults to assignment configuration and database-backed policies. +- Version queue payloads when adding fields to avoid producer/consumer drift. +- Keep execution polling backward compatible for frontend stability. diff --git a/llms/backend/messaging/README.md b/llms/backend/messaging/README.md new file mode 100644 index 0000000..ff3eefa --- /dev/null +++ b/llms/backend/messaging/README.md @@ -0,0 +1,51 @@ +# Backend Messaging Implementation + +## Scope +This document covers backend message production/consumption for code execution workflows. + +Key classes: +- config/RabbitConfig.java +- messaging/producer/ExecutionRequestProducer.java +- messaging/listener/ExecutionResultListener.java + +## Queue Topology +Configured in RabbitConfig: +- Request queue: codehive_queue (default, configurable by system property) +- Result queue: codehive_result_queue (default, configurable by system property) + +Message converter: +- Jackson2JsonMessageConverter for JSON serialization/deserialization. + +## Producer Flow +ExecutionRequestProducer: +1. Receives ExecutionJob from service layer. +2. Logs execution context and limits. +3. Publishes to request queue using RabbitTemplate.convertAndSend. + +Produced payload model: +- model/dto/queue/ExecutionJob.java + +## Consumer Flow +ExecutionResultListener: +1. Listens on configured result queue. +2. Receives ExecutionReport payload. +3. Delegates to ExecutionResultService for database update. +4. Logs processing success/failures. + +Consumed payload model: +- model/dto/queue/ExecutionReport.java + +## Reliability Notes +- Queues are declared durable in RabbitConfig. +- Listener catches processing errors and logs them to prevent silent failures. +- Producer rethrows publish exceptions. + +## Compatibility Rules +- Treat queue DTO changes as contract changes. +- If fields are added, coordinate worker and backend deployment. +- Keep default queue names stable unless infrastructure update is coordinated. + +## Operational Tips +- Verify backend and worker use matching queue names. +- Keep RabbitMQ running before submitting execution requests. +- Use execution logs to trace request id and status transitions. diff --git a/llms/backend/model/README.md b/llms/backend/model/README.md new file mode 100644 index 0000000..7c52308 --- /dev/null +++ b/llms/backend/model/README.md @@ -0,0 +1,154 @@ +# Backend Model Implementation + +## Scope +This document describes backend data modeling: entities, DTOs, mappers, request/response contracts, enums, and exception structure. + +Package root: +- codehive-backend/src/main/java/com/github/codehive/model + +## Entity Model +Entities are plain JPA classes with explicit constraints via @Column and relationship annotations. + +### User +File: model/entity/User.java + +Important constraints: +- name: nullable=false, length=50 +- lastName: nullable=false, length=80 +- enrollmentNumber: nullable=false, unique=true, length=50 +- email: nullable=false, unique=true, length=100 +- password: nullable=false, length=255 +- role: enum stored as string, length=15 + +Behavior notes: +- Implements UserDetails for Spring Security integration. +- createdAt defaults to LocalDateTime.now(). +- isActive defaults to true. +- temporaryPassword defaults to false. +- scopes stored as element collection in user_scopes table. + +### PasswordResetToken +File: model/entity/PasswordResetToken.java + +Important constraints: +- token unique and non-null. +- expiryDate non-null. +- used non-null default false. +- ManyToOne relation to User (nullable=false). + +### Assignment +File: model/entity/Assignment.java + +Important constraints: +- title length 200, non-null. +- description TEXT, non-null. +- timeLimitMs and memoryLimitMb non-null. +- comparatorType enum string, non-null. +- createdAt and updatedAt non-null. + +Collection-backed tables: +- assignment_constraints +- assignment_hints +- assignment_tags +- assignment_allowed_languages + +### Submission +File: model/entity/Submission.java +- ManyToOne assignment relation (non-null). +- language enum string (non-null). +- createdAt initialized in constructor. + +### Execution +File: model/entity/Execution.java + +Important constraints and semantics: +- executionType and status are required enums. +- status defaults to PENDING. +- isOutdated defaults to false. +- submission is nullable (practice executions may have no submission). +- user relation is nullable (depends on requester context). + +### TestCase +File: model/entity/TestCase.java +- assignment relation required. +- order stored as order_index, non-null. +- isSample default false. + +### ReferenceSolution +File: model/entity/ReferenceSolution.java +- assignment relation required. +- language enum required. + +## Request Contract Structure +Request classes live under model/request grouped by domain: +- auth/LoginRequest +- auth/SignUpRequest +- recovery/ForgotPasswordRequest +- recovery/RecoveryPasswordRequest +- execution/ExecutionRequest + +Validation patterns: +- @NotBlank for required strings. +- @NotNull for required enum fields. +- @Size for password and text boundaries. +- @Email for email format in signup. + +Examples of enforced constraints: +- login password min length 6. +- signup name 2..50. +- signup father/mother last name 2..40. +- recovery new password min length 6. + +## Response Contract Structure +Base response pattern: +- ApiResponse(success, message) + +Concrete wrappers: +- SuccessResponse for successful payload responses. +- ErrorResponse for timestamped API failures. +- MessageResponse for simple text payloads. + +Auth-specific responses: +- auth/AuthResponse (token + UserDTO) +- auth/CsvBulkRegisterResponse +- auth/CsvProgressMessage + +## DTO and Mapper Layer +DTOs mirror API-safe views and queue contracts. + +Application DTO examples: +- UserDTO +- ExecutionDTO +- AssignmentDTO +- SubmissionDTO + +Queue DTO examples: +- queue/ExecutionJob +- queue/ExecutionReport + +Mappers convert entity <-> DTO, for example: +- ExecutionMapper +- UserMapper +- AssignmentMapper + +## Enum Strategy +Enums are persisted and transferred as string values: +- Role, Scope +- Language +- ExecutionType +- ExecutionStatus +- ComparatorType + +## Exception Model +Exception packages are domain-grouped: +- exception/auth +- exception/recovery +- exception/handler + +GlobalExceptionHandler maps exceptions to stable HTTP responses and ErrorResponse payloads. + +## Practical Modeling Rules +- Keep entity constraints aligned with request validation, not looser. +- Prefer DTO exposure over returning entities directly. +- If adding enum values, validate impact on frontend and worker contracts. +- Queue DTO evolution must be coordinated across services. diff --git a/llms/backend/security/README.md b/llms/backend/security/README.md new file mode 100644 index 0000000..7136c3b --- /dev/null +++ b/llms/backend/security/README.md @@ -0,0 +1,62 @@ +# Backend Security Implementation + +## Scope +This document describes authentication, authorization, and filter-chain behavior in codehive-backend. + +Primary classes: +- config/SecurityConfig.java +- security/JWTAuthenticationFilter.java +- security/UserDetailsServiceImplementation.java +- utils/JwtUtil.java + +## Security Model +- Stateless authentication with JWT bearer tokens. +- User principal loaded from users table via UserDetailsService. +- Role-based authorization via authorities derived from Role enum. +- Method-level authorization enabled with @EnableMethodSecurity. + +## Filter Chain Behavior +JWTAuthenticationFilter executes once per request: +1. Reads Authorization header. +2. Verifies Bearer token prefix. +3. Extracts subject (email) from token. +4. Loads user details from repository. +5. Validates token signature and expiration. +6. Populates SecurityContext if valid. + +Invalid or missing token behavior: +- Filter does not authenticate and request continues. +- Access depends on endpoint permitAll/authenticated rules. + +## Authorization Rules +Configured in SecurityConfig: +- Permit all: + - POST /api/auth/login + - POST /api/recovery-password/** + - /api/execution/** (currently open) + - /ws/** + - Swagger endpoints +- All other routes require authentication. + +Method-level restrictions: +- Admin-only actions use @PreAuthorize("hasAuthority('ADMIN')"). + +## Password and Identity Handling +- Password hashing: BCryptPasswordEncoder. +- UserDetails identity key: email. +- Locked/disabled semantics depend on User.isActive. + +## CORS and Session +- CORS currently allows all origins through allowed origin pattern. +- Session creation policy is STATELESS. +- CSRF is disabled for API style usage. + +## Related Error Handling +Security-related exceptions are translated by GlobalExceptionHandler: +- InvalidJWTException / ExpiredJWTException -> 401 +- AccessDeniedException -> 403 + +## Hardening Notes +- /api/execution/** is currently permitAll and should be reviewed if requester identity becomes mandatory. +- CORS allow-all is convenient for development but should be narrowed for production. +- Keep JWT secret/expiration in environment configuration and rotate when required. diff --git a/llms/backend/service/README.md b/llms/backend/service/README.md new file mode 100644 index 0000000..e0f0f9f --- /dev/null +++ b/llms/backend/service/README.md @@ -0,0 +1,97 @@ +# Backend Service Layer Implementation + +## Scope +This document explains service-layer responsibilities and coordination patterns in codehive-backend. + +Service classes: +- AuthService +- RecoveryPasswordService +- CsvRegistrationService +- ExecutionRequestService +- ExecutionResultService +- ObjectStorageService +- MailSenderService + +## Service Layer Role +- Own business logic and workflows. +- Orchestrate repositories, messaging, storage, and integrations. +- Keep controllers thin and persistence details encapsulated. +- Enforce transactional boundaries where needed. + +## AuthService +Responsibilities: +- Login using email or enrollment number identifier. +- Admin-driven user registration. +- CSV batch registration support. +- JWT issuance and user retrieval by token. + +Key dependencies: +- UserRepository +- PasswordEncoder +- JwtUtil +- MailSenderService + +## RecoveryPasswordService +Responsibilities: +- Handle forgot-password with neutral response messaging. +- Invalidate previous unused reset tokens. +- Generate and persist a new token with 15-minute expiry. +- Validate token (exists, not expired, not used) and update password. + +Key dependencies: +- PasswordResetTokenRepository +- UserRepository +- PasswordEncoder +- MailSenderService + +## CsvRegistrationService +Responsibilities: +- Async parsing and processing of CSV rows. +- Row-by-row validation and persistence. +- Progress and completion events through WebSocket handler. + +Execution model: +- Runs with @Async. +- Uses taskId routing to send progress to subscribed clients. + +## ExecutionRequestService +Responsibilities: +- Create execution records. +- Persist source code to object storage. +- Build ExecutionJob payload. +- Send execution request to RabbitMQ. + +Current implementation notes: +- Contains TODOs indicating current behavior is oriented to manual test execution. +- Uses default time/memory/comparator values pending assignment-driven policies. + +## ExecutionResultService +Responsibilities: +- Consume worker reports (via listener call chain). +- Update execution status and resource metrics. +- Persist result summary for polling clients. + +## ObjectStorageService +Responsibilities: +- Upload and download artifacts from MinIO bucket. +- Support both stream uploads and plain-text content uploads. + +## MailSenderService +Responsibilities: +- Send password recovery emails. +- Send welcome emails with temporary credentials. + +Configuration-driven fields: +- frontend.url +- spring.mail.username + +## Transaction and Error Patterns +- Write operations use @Transactional. +- Read-only fetches use @Transactional(readOnly = true) where applicable. +- Domain-specific exceptions are propagated and translated by GlobalExceptionHandler. + +## Service Conventions +- Keep each service focused by domain. +- Avoid direct HTTP concerns in services. +- Keep queue and storage payload-building deterministic. +- Prefer explicit logging at workflow boundaries for observability. diff --git a/llms/frontend/OVERVIEW.md b/llms/frontend/OVERVIEW.md new file mode 100644 index 0000000..1340849 --- /dev/null +++ b/llms/frontend/OVERVIEW.md @@ -0,0 +1,67 @@ +# Frontend Overview + +## What This Component Does +The frontend is a React Router v7 single-page application. It provides the user interface for authentication, account recovery, role-based navigation, and administration flows. + +Main responsibilities: +- Render pages and reusable UI components. +- Manage client-side routing. +- Call backend APIs through service modules. +- Store and expose auth/theme state through context providers. +- Guard protected routes based on authentication state. + +## How It Works +Typical page flow: +1. User navigates to a route. +2. Route component loads and renders page-level UI. +3. Page calls service methods for API communication when needed. +4. Context providers supply shared state (for example auth state). +5. Components render success, loading, and error states based on API results. + +Authentication flow at a high level: +1. User submits credentials on login page. +2. Auth service calls backend endpoint. +3. Auth context updates session state. +4. Protected routes allow or block access accordingly. + +## Useful Commands +Run these from codehive-frontend. + +Development: +- npm install +- npm run dev + +Build and serve: +- npm run build +- npm run start + +Type safety: +- npm run typecheck + +## Project Folder Structure +Runtime module structure (codehive-frontend/app): +- root.tsx: Root app shell and provider wiring. +- routes.ts: Route registration. +- routes: Route entry files. +- pages: Page-level components. +- components: Reusable UI building blocks. +- services: API clients and request helpers. +- types: Shared TypeScript types. +- context: App-wide state providers. +- welcome: Welcome-related route content. +- app.css: Global styling. + +Frontend docs structure in llms/frontend: +- OVERVIEW.md: This document. +- admin: Admin feature behavior and constraints. +- components: Shared component guidelines. +- professor: Professor-facing workflows. +- routes: Route-level behavior and conventions. +- services: API service contracts and patterns. +- student: Student-facing workflows. + +## How To Navigate Frontend Docs +Read in this order: +1. llms/frontend/OVERVIEW.md +2. The relevant domain folder in llms/frontend +3. The corresponding folder in codehive-frontend/app diff --git a/llms/frontend/admin/README.md b/llms/frontend/admin/README.md new file mode 100644 index 0000000..2597d3f --- /dev/null +++ b/llms/frontend/admin/README.md @@ -0,0 +1,66 @@ +# Frontend Admin Implementation + +## Scope +This document explains the admin-facing frontend implementation currently available in codehive-frontend. + +## Route Entry Points +Admin routes are declared in app/routes.ts: +- /admin +- /admin/create-user +- /admin/csv-upload + +Route modules: +- app/routes/admin.tsx +- app/routes/admin.create-user.tsx +- app/routes/admin.csv-upload.tsx + +Each admin route wraps pages with: +- ThemeProvider +- ProtectedRoute roles={[Role.ADMIN]} + +This guarantees role-gated rendering for admin pages. + +## Admin Pages +Page implementations: +- app/pages/admin/AdminDashboardPage.tsx +- app/pages/admin/CreateUserPage.tsx +- app/pages/admin/CsvUploadPage.tsx + +### AdminDashboardPage +- Entry navigation to create-user and csv-upload features. +- Theme toggle and quick placeholders for summary metrics. + +### CreateUserPage +- Form to create one user account. +- Calls AuthService.signUp. +- Handles loading, success, and error states. + +Input model aligns with backend signup contract: +- role +- name +- fatherLastName +- motherLastName +- enrollmentNumber +- email + +### CsvUploadPage +- Uploads CSV through AuthService.uploadCsv. +- Uses backend taskId and WebSocket subscription to stream progress. +- Shows processed/success/failure counts and row-level errors. +- Handles WebSocket lifecycle with cleanup on unmount. + +## Dependencies and Contracts +Service dependency: +- app/services/AuthService.ts + +Type dependency: +- app/types (Role, SignUpRequest, CsvProgressMessage, response wrappers) + +## Security and Access Pattern +- Admin route protection is enforced in frontend by ProtectedRoute. +- Backend still remains source of truth for authorization. + +## Extension Guidance +- Keep admin flows under app/pages/admin and app/routes/admin.*. +- Reuse AuthService and typed contracts instead of duplicating fetch logic. +- For new admin features, include role-gated route wrappers by default. diff --git a/llms/frontend/components/README.md b/llms/frontend/components/README.md new file mode 100644 index 0000000..0c57c15 --- /dev/null +++ b/llms/frontend/components/README.md @@ -0,0 +1,54 @@ +# Frontend Components Implementation + +## Scope +This document describes reusable UI and guard components in app/components. + +Component files: +- app/components/Header.tsx +- app/components/Hero.tsx +- app/components/Features.tsx +- app/components/HowItWorks.tsx +- app/components/Testimonials.tsx +- app/components/Contact.tsx +- app/components/Footer.tsx +- app/components/ProtectedRoute.tsx + +## Structural Components +Landing page sections are split into reusable blocks: +- Header and hero sections +- feature explanation sections +- testimonial and contact sections +- footer + +These are composed by landing page modules rather than tightly coupled to route files. + +## Access Control Component +ProtectedRoute is the core guard component. + +Behavior: +1. Checks token presence through AuthService.getToken. +2. Calls AuthService.getMe to resolve the current user. +3. Optionally validates role list and scope list. +4. Redirects unauthorized users to role dashboard or login. +5. Renders loading state while auth check is pending. + +Current redirect mapping: +- ADMIN -> /admin +- default -> / + +## Context Dependencies +ProtectedRoute relies on: +- app/services/AuthService.ts +- app/types for Role and Scope + +Other components rely on theme classes and app-level styling from app.css. + +## Reuse Rules +- Keep data-fetching and auth checks out of visual section components. +- Place access logic in ProtectedRoute or route-level wrappers. +- Preserve component-level responsibility: presentational or guard, not both. + +## Extension Guidance +- New reusable sections should be added to app/components. +- New protected pages should use ProtectedRoute at route-level composition. +- If role routes expand, update getDashboardRoute behavior in ProtectedRoute. diff --git a/llms/frontend/professor/README.md b/llms/frontend/professor/README.md new file mode 100644 index 0000000..65c0c02 --- /dev/null +++ b/llms/frontend/professor/README.md @@ -0,0 +1,14 @@ +# Professor Frontend Placeholder + +Professor-specific pages are not implemented yet. + +Current status: +- No dedicated professor routes. +- No professor-only page modules. +- No professor-specific service layer logic in frontend. + +When implementation starts, document here: +1. Route map and entry files. +2. Page-level flows and permissions. +3. Service/API integrations. +4. Shared components and role-based guards. diff --git a/llms/frontend/routes/README.md b/llms/frontend/routes/README.md new file mode 100644 index 0000000..a29c3c6 --- /dev/null +++ b/llms/frontend/routes/README.md @@ -0,0 +1,56 @@ +# Frontend Routes Implementation + +## Scope +This document explains route registration and route-module behavior for the React Router v7 frontend. + +## Route Registry +Central route declaration: +- app/routes.ts + +Current route map: +- / -> app/routes/home.tsx +- /login -> app/routes/login.tsx +- /forgot-password -> app/routes/forgot-password.tsx +- /reset-password -> app/routes/reset-password.tsx +- /admin -> app/routes/admin.tsx +- /admin/create-user -> app/routes/admin.create-user.tsx +- /admin/csv-upload -> app/routes/admin.csv-upload.tsx + +## Route Module Pattern +Each route module typically contains: +1. meta export for SEO metadata. +2. Default export component that renders page composition. + +Common composition strategy: +- Public routes: render page directly, often wrapped with ThemeProvider. +- Protected admin routes: ThemeProvider + ProtectedRoute + admin page. + +## Root and Layout +Root file: +- app/root.tsx + +Key responsibilities: +- HTML shell and global links. +- preconnect and font setup. +- inline theme bootstrap script to avoid flash. +- ScrollRestoration and Scripts. +- shared ErrorBoundary behavior. + +## Page Binding +Routes map to page modules under app/pages: +- LandingPage +- LoginPage +- RecoveryPasswordPage +- ResetPasswordPage +- admin/* pages + +## Guarding Strategy +Role-sensitive routes rely on ProtectedRoute. +- Current protected area is admin. +- Role checks occur client-side using /api/auth/me. + +## Extension Guidance +- Add new path entries in app/routes.ts first. +- Keep route files thin and move UI into page components. +- Apply ProtectedRoute in route composition for role/scoped pages. +- Keep metadata maintained per route for SEO and social previews. diff --git a/llms/frontend/services/README.md b/llms/frontend/services/README.md new file mode 100644 index 0000000..413a78c --- /dev/null +++ b/llms/frontend/services/README.md @@ -0,0 +1,64 @@ +# Frontend Services Implementation + +## Scope +This document describes API service modules used by the frontend. + +Service files: +- app/services/AuthService.ts +- app/services/RecoveryPasswordService.ts +- app/services/index.ts + +Base URL strategy: +- Uses VITE_API_URL when provided. +- Falls back to http://localhost:8080. + +## AuthService +Base path: +- /api/auth + +Implemented methods: +- login(credentials) +- signUp(userData) +- uploadCsv(file) +- getMe() +- getWebSocketUrl() +- setToken/getToken/removeToken/isAuthenticated/logout + +Behavior details: +- signUp and uploadCsv include Authorization Bearer token. +- uploadCsv uses multipart/form-data. +- getMe validates token by calling /me. +- token storage is localStorage key authToken. +- websocket URL rewrites http(s) base to ws(s) and appends /ws/csv-progress. + +## RecoveryPasswordService +Base path: +- /api/recovery-password + +Implemented methods: +- forgotPassword(request) +- resetPassword(request) + +Behavior details: +- Both methods post JSON payloads. +- Non-2xx responses throw Error with API message fallback. + +## Type Contracts +Services consume and return typed contracts from app/types: +- request types (login/signup/forgot/reset) +- response wrappers (SuccessResponse, ErrorResponse) +- domain payloads (AuthResponse, MessageResponse, CsvTaskResponse, User) + +## Error Handling Pattern +- Parse JSON response. +- If response not ok, throw Error using backend message/error fields. +- Let page or route components handle user-facing error display. + +## Re-export Pattern +app/services/index.ts re-exports service singletons for concise imports: +- import { AuthService, RecoveryPasswordService } from ~/services + +## Extension Guidance +- Keep one service class per backend API domain. +- Centralize token/header behavior in services, not components. +- Preserve response wrappers to maintain consistency across pages. diff --git a/llms/frontend/student/README.md b/llms/frontend/student/README.md new file mode 100644 index 0000000..83a497a --- /dev/null +++ b/llms/frontend/student/README.md @@ -0,0 +1,14 @@ +# Student Frontend Placeholder + +Student-specific pages are not implemented yet. + +Current status: +- No dedicated student routes. +- No student-only page modules. +- No student-specific service layer logic in frontend. + +When implementation starts, document here: +1. Route map and entry files. +2. Page-level workflows and permissions. +3. Service/API integrations. +4. Shared components and access controls. diff --git a/llms/worker/OVERVIEW.md b/llms/worker/OVERVIEW.md new file mode 100644 index 0000000..2e6a898 --- /dev/null +++ b/llms/worker/OVERVIEW.md @@ -0,0 +1,71 @@ +# Worker Overview + +## What This Component Does +The worker is a background Spring Boot service responsible for sandboxed code execution. It consumes execution jobs from RabbitMQ, runs code in isolated environments, evaluates outputs, and publishes results back to the system. + +Main responsibilities: +- Consume execution jobs from message queues. +- Select the proper language executor. +- Run untrusted code with sandbox and timeout constraints. +- Compare produced output with expected output when required. +- Publish execution results to backend-facing queues. + +## How It Works +Execution pipeline: +1. Backend publishes an execution job message. +2. Worker listener receives and validates the job. +3. Worker fetches required assets and input data. +4. Worker selects a language executor through a factory. +5. Code is compiled/run inside constrained execution environments. +6. Comparator logic validates output when test-based execution is used. +7. Worker publishes the final execution result message. + +Safety and reliability concerns: +- Timeout protection prevents runaway executions. +- Language-specific executors isolate compile/run logic. +- Messaging decouples API traffic from execution workload. + +## Useful Commands +Run these from codehive-worker. + +Build and run: +- ./gradlew build +- ./gradlew bootRun + +Testing: +- ./gradlew test + +Operational dependency: +- RabbitMQ must be running before bootRun. + +Typical local setup: +1. Start infrastructure from codehive-backend using docker compose up -d. +2. Run backend. +3. Run worker. + +## Project Folder Structure +Runtime module structure (codehive-worker/src/main/java/com/github/codehive/worker): +- config: Worker infrastructure configuration. +- messaging: Queue listeners and producers. +- model: Worker DTOs and enums. +- sandbox: Execution core and language implementations. +- service: Worker service-level integrations. +- WorkerApplication.java: Spring Boot entry point. + +Sandbox structure highlights: +- sandbox/factory: Executor selection. +- sandbox/java, sandbox/python, sandbox/c, sandbox/cpp: Language-specific execution strategies. +- sandbox/LanguageExecutor.java: Executor contract. + +Worker docs structure in llms/worker: +- OVERVIEW.md: This document. +- comparator: Output comparison behavior and rules. +- execution: Execution lifecycle and orchestration. +- messaging: Queue payloads and message flow. +- sandbox: Isolation, execution strategy, and constraints. + +## How To Navigate Worker Docs +Read in this order: +1. llms/worker/OVERVIEW.md +2. The relevant domain folder in llms/worker +3. The corresponding package in codehive-worker diff --git a/llms/worker/comparator/README.md b/llms/worker/comparator/README.md new file mode 100644 index 0000000..49c31fc --- /dev/null +++ b/llms/worker/comparator/README.md @@ -0,0 +1,50 @@ +# Worker Comparator Implementation + +## Scope +This document explains how output comparison is implemented in the worker. + +Main class: +- codehive-worker/src/main/java/com/github/codehive/worker/service/OutputComparatorService.java + +Comparator enum: +- codehive-worker/src/main/java/com/github/codehive/worker/model/enums/ComparatorType.java + +## Supported Comparator Types +- EXACT_MATCH +- FLOATING_POINT + +## Comparison Workflow +1. TestExecutionService executes submission and obtains actual output. +2. Expected output is resolved from: + - definitive mode: .out files from object storage + - practice mode: reference solution execution output +3. OutputComparatorService.compareWithFeedback is called. +4. Result includes: + - matches boolean + - feedback message for diagnostics +5. If comparison fails, test case status is set to WA. + +## EXACT_MATCH Behavior +- Normalizes output into trimmed non-empty lines. +- Requires same line count and exact line equality. +- Returns detailed mismatch feedback including line number and truncated diff values. + +## FLOATING_POINT Behavior +- Also normalizes lines. +- Compares token-by-token. +- Tries numeric parse first and applies epsilon tolerance of 1e-9. +- Falls back to string equality for non-numeric tokens. + +## Error and Fallback Rules +- If expected and actual are both null, comparison passes. +- If only one is null, comparison fails. +- Unknown comparator type falls back to EXACT_MATCH with warning logs. + +## Integration Points +- Caller: TestExecutionService +- Affects: TestCaseResult.status and feedback +- Contributes to: ExecutionReport overall status determination + +## Extension Guidance +- Add comparator enum values in ComparatorType and implement branch handling in OutputComparatorService. +- Keep feedback deterministic and concise, since it is user-facing diagnostic text. diff --git a/llms/worker/execution/README.md b/llms/worker/execution/README.md new file mode 100644 index 0000000..2677628 --- /dev/null +++ b/llms/worker/execution/README.md @@ -0,0 +1,74 @@ +# Worker Execution Lifecycle + +## Scope +This document describes how the worker executes incoming jobs, computes verdicts, and stores result artifacts. + +Primary classes: +- codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/ExecutionRequestListener.java +- codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java +- codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionReport.java +- codehive-worker/src/main/java/com/github/codehive/worker/model/dto/TestCaseResult.java + +## High-Level Flow +1. ExecutionRequestListener receives ExecutionJob from RabbitMQ. +2. Listener delegates job to TestExecutionService.executeJob. +3. Service compiles/runs submission through the selected language executor. +4. Per-test results are aggregated in ExecutionReport. +5. Report JSON and test artifacts are uploaded to object storage. +6. Listener publishes final ExecutionReport to result queue. + +## Execution Modes +DEFINITIVE: +- Uses testsPath and numTests from ExecutionJob. +- For each test case: + - download input (.in) + - download expected output (.out) + - execute submission + - compare outputs + +PRACTICE: +- Uses inline testCases from ExecutionJob. +- Runs reference solution first to generate expected output. +- Runs submission and compares against reference output. + +## Compilation Gate +- Service performs an initial compile/syntax validation pass before test loop. +- If compile fails: + - report.compilationError is set + - overall status becomes CE + - per-test execution is skipped + +## Test Artifact Output +For each test case, service uploads: +- stdout.txt +- stderr.txt (if non-empty) + +Report upload: +- report.json generated from ExecutionReport and uploaded to outputPath. + +## Verdict Model +Per-test verdicts are represented by ExecutionStatus values: +- AC, WA, CE, RTE, TLE, MLE, PENDING + +Overall verdict in ExecutionReport is derived by priority: +1. CE if compilationError exists +2. AC if all tests pass +3. otherwise TLE > MLE > RTE > WA + +## Runtime Constraints +Each run uses limits from ExecutionJob: +- timeLimitMs +- memoryLimitMb + +Executors enforce limits in Docker using container constraints and timeout guards. + +## Failure Handling +- Unhandled processing failures create an error report with RTE and compilationError summary. +- Listener still publishes error result to backend queue to avoid silent job loss. + +## Extension Guidance +- Keep ExecutionJob and ExecutionReport contract-compatible with backend. +- If adding new statuses, update: + - ExecutionStatus enum + - overall status calculation + - backend result processing logic diff --git a/llms/worker/messaging/README.md b/llms/worker/messaging/README.md new file mode 100644 index 0000000..b7cbd12 --- /dev/null +++ b/llms/worker/messaging/README.md @@ -0,0 +1,62 @@ +# Worker Messaging Implementation + +## Scope +This document describes how the worker consumes execution jobs and publishes execution results through RabbitMQ. + +Key classes: +- codehive-worker/src/main/java/com/github/codehive/worker/config/RabbitMQConfig.java +- codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/ExecutionRequestListener.java +- codehive-worker/src/main/java/com/github/codehive/worker/messaging/producer/ExecutionResultProducer.java + +## Queue Topology +Configured queue names: +- request queue: codehive_queue (default) +- result queue: codehive_result_queue (default) + +Both queues are durable and use Jackson JSON message conversion. + +## Incoming Message Contract +Listener consumes: +- model/dto/queue/ExecutionJob + +Important fields in ExecutionJob: +- id +- source +- reference +- language +- referenceLanguage +- executionType +- comparatorType +- timeLimitMs +- memoryLimitMb +- outputPath +- testsPath, numTests, testCases + +## Consumption Flow +1. @RabbitListener receives ExecutionJob. +2. Listener logs full job context. +3. Listener invokes TestExecutionService.executeJob. +4. Successful run produces ExecutionReport. +5. Listener sends report through ExecutionResultProducer. + +Error path: +- If execution fails unexpectedly, listener builds fallback report: + - overallStatus = RTE + - compilationError = internal error message +- Fallback report is still published to backend. + +## Outgoing Message Contract +Producer publishes: +- model/dto/ExecutionReport + +Report includes: +- executionId +- overallStatus +- per-test results +- timing and memory summaries +- compilationError when present + +## Operational Notes +- Backend and worker must share queue names and compatible DTO schemas. +- Messaging logs are the first place to debug job handoff issues. +- Keep payload evolution coordinated across both services. diff --git a/llms/worker/sandbox/README.md b/llms/worker/sandbox/README.md new file mode 100644 index 0000000..fe83d30 --- /dev/null +++ b/llms/worker/sandbox/README.md @@ -0,0 +1,91 @@ +# Worker Sandbox Implementation + +## Scope +This document explains how language executors run code in isolated Docker containers with time and memory constraints. + +Core abstraction: +- codehive-worker/src/main/java/com/github/codehive/worker/sandbox/LanguageExecutor.java + +Factory: +- codehive-worker/src/main/java/com/github/codehive/worker/sandbox/factory/LanguageExecutorFactory.java + +Executors: +- JAVA: sandbox/java/JavaExecutor.java +- PYTHON: sandbox/python/PythonExecutor.java +- C: sandbox/c/CExecutor.java +- CPP: sandbox/cpp/CPPExecutor.java + +## Executor Contract +LanguageExecutor.execute arguments: +- sourceCode input stream +- testInput input stream (nullable) +- timeLimitMs +- memoryLimitMb + +Returns ExecutionResult with: +- status +- output / errorOutput +- executionTimeMs +- memoryUsedMb +- exitCode +- compilationError (when applicable) + +## Isolation Strategy +Each execution uses temporary filesystem workspace and Docker container isolation. + +Container restrictions: +- no network access (networkMode none) +- memory hard limit and swap disabled +- CPU quota for bounded CPU usage + +Timeout control: +- WaitContainer uses Future timeout based on timeLimitMs. +- timeout leads to TLE. + +Memory limit signaling: +- exit code 137 is interpreted as likely OOM and mapped to MLE. + +## Language-Specific Behavior +Java: +- compile step with javac Main.java +- run step with java Main + +Python: +- no explicit compile step +- syntax errors are treated as CE when stderr includes SyntaxError + +C: +- compile with gcc -o program main.c -lm +- run compiled binary + +C++: +- compile with g++ -o program main.cpp -std=c++17 -lm +- run compiled binary + +All executors: +- optionally pipe input.txt when test input exists +- capture stdout and stderr from container logs +- attempt to read peak memory via Docker stats snapshot +- clean up container and temporary directory in finally blocks + +## Image Management +On executor initialization: +- inspect configured base image +- pull image if missing + +Default images: +- Java: eclipse-temurin:21-jdk-ubi10-minimal +- Python: python:3.11-slim +- C/C++: gcc:latest + +## Factory Resolution +LanguageExecutorFactory receives Spring component map keyed by bean names. +- Bean names are language enum names (JAVA, PYTHON, C, CPP). +- Unknown or null language throws IllegalArgumentException. + +## Extension Guidance +- To add a new language: + 1. implement LanguageExecutor + 2. register Spring component with enum-name bean key + 3. extend Language enum and backend mapping + 4. verify ExecutionJob contract supports new language From 145086bf2bddd31922e4161af55dba9b32c74c6d Mon Sep 17 00:00:00 2001 From: IrminDev Date: Fri, 8 May 2026 21:48:35 -0600 Subject: [PATCH 14/32] Changed the use of Long ids for UUIDs --- AGENTS.md | 8 ++++ .../controller/CheckExecutionController.java | 4 +- .../codehive/model/dto/AssignmentDTO.java | 7 ++-- .../codehive/model/dto/ExecutionDTO.java | 19 +++++---- .../model/dto/ReferenceSolutionDTO.java | 14 ++++--- .../codehive/model/dto/SubmissionDTO.java | 13 +++--- .../codehive/model/dto/TestCaseDTO.java | 13 +++--- .../github/codehive/model/dto/UserDTO.java | 7 ++-- .../model/dto/queue/ExecutionJob.java | 11 ++--- .../model/dto/queue/ExecutionReport.java | 10 +++-- .../codehive/model/entity/Assignment.java | 9 ++-- .../codehive/model/entity/Execution.java | 9 ++-- .../model/entity/PasswordResetToken.java | 9 ++-- .../model/entity/ReferenceSolution.java | 10 +++-- .../codehive/model/entity/Submission.java | 9 ++-- .../codehive/model/entity/TestCase.java | 10 +++-- .../github/codehive/model/entity/User.java | 9 ++-- .../request/execution/ExecutionRequest.java | 19 +++++---- .../repository/AssignmentRepository.java | 5 ++- .../repository/ExecutionRepository.java | 3 +- .../PasswordResetTokenRepository.java | 3 +- .../ReferenceSolutionRepository.java | 5 ++- .../repository/SubmissionRepository.java | 11 ++--- .../repository/TestCaseRepository.java | 21 +++++----- .../codehive/repository/UserRepository.java | 3 +- .../github/codehive/service/AuthService.java | 3 +- .../service/ExecutionRequestService.java | 4 +- .../codehive/utils/ObjectKeyBuilder.java | 35 ++++++++-------- .../CsvProgressWebSocketHandler.java | 5 ++- .../codehive/model/mapper/UserMapperTest.java | 42 +++++++++++-------- .../codehive/service/AuthServiceTest.java | 19 +++++---- .../service/RecoveryPasswordServiceTest.java | 11 ++--- .../app/shared/types/model/User.ts | 2 +- .../worker/model/dto/ExecutionReport.java | 13 +++--- .../worker/model/dto/queue/ExecutionJob.java | 11 ++--- 35 files changed, 220 insertions(+), 166 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d80cf66..6ff2bfc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -126,6 +126,14 @@ High-level architecture: - Worker consumes execution jobs, runs sandboxed code, and publishes execution results. - Shared infrastructure includes RabbitMQ and object storage. +## ID Convention +All entity primary keys use `java.util.UUID` (not `Long`). +- JPA entities: `@GeneratedValue(strategy = GenerationType.UUID)` +- Repositories: `JpaRepository` +- DTOs, request/response models, and queue DTOs: `UUID` fields +- Controllers: `@PathVariable UUID id` +- Frontend: entity IDs are typed as `string` + For concrete architecture, contracts, data models, and execution flow, always consult: - llms/backend/OVERVIEW.md - llms/frontend/OVERVIEW.md diff --git a/codehive-backend/src/main/java/com/github/codehive/controller/CheckExecutionController.java b/codehive-backend/src/main/java/com/github/codehive/controller/CheckExecutionController.java index 4f76924..0e2c465 100644 --- a/codehive-backend/src/main/java/com/github/codehive/controller/CheckExecutionController.java +++ b/codehive-backend/src/main/java/com/github/codehive/controller/CheckExecutionController.java @@ -1,5 +1,7 @@ package com.github.codehive.controller; +import java.util.UUID; + import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -102,7 +104,7 @@ public ResponseEntity> submitExecution( @GetMapping("/check/{id}") public ResponseEntity> getExecution( @Parameter(description = "Execution ID", required = true) - @PathVariable Long id) { + @PathVariable UUID id) { logger.info("[WORKFLOW] GET /api/execution/check/{} - Fetching execution status", id); ExecutionDTO execution = executionRequestService.getExecutionById(id); diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/AssignmentDTO.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/AssignmentDTO.java index 439c2c0..a7514bb 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/dto/AssignmentDTO.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/AssignmentDTO.java @@ -2,6 +2,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.UUID; import com.fasterxml.jackson.annotation.JsonInclude; import com.github.codehive.model.enums.ComparatorType; @@ -9,7 +10,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class AssignmentDTO { - private Long id; + private UUID id; private String title; private String description; private List constraints; @@ -32,11 +33,11 @@ public void setIsActive(Boolean isActive) { this.isActive = isActive; } - public Long getId() { + public UUID getId() { return id; } - public void setId(Long id) { + public void setId(UUID id) { this.id = id; } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/ExecutionDTO.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/ExecutionDTO.java index c195571..85d2cff 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/dto/ExecutionDTO.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/ExecutionDTO.java @@ -1,6 +1,7 @@ package com.github.codehive.model.dto; import java.time.LocalDateTime; +import java.util.UUID; import com.fasterxml.jackson.annotation.JsonInclude; import com.github.codehive.model.enums.ExecutionStatus; @@ -8,9 +9,9 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class ExecutionDTO { - private Long id; - private Long submissionId; - private Long userId; + private UUID id; + private UUID submissionId; + private UUID userId; private ExecutionType executionType; private ExecutionStatus status; private Long timeMs; @@ -18,27 +19,27 @@ public class ExecutionDTO { private Boolean isOutdated; private LocalDateTime createdAt; - public Long getId() { + public UUID getId() { return id; } - public void setId(Long id) { + public void setId(UUID id) { this.id = id; } - public Long getSubmissionId() { + public UUID getSubmissionId() { return submissionId; } - public void setSubmissionId(Long submissionId) { + public void setSubmissionId(UUID submissionId) { this.submissionId = submissionId; } - public Long getUserId() { + public UUID getUserId() { return userId; } - public void setUserId(Long userId) { + public void setUserId(UUID userId) { this.userId = userId; } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/ReferenceSolutionDTO.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/ReferenceSolutionDTO.java index eb8f491..dd1ab5a 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/dto/ReferenceSolutionDTO.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/ReferenceSolutionDTO.java @@ -1,27 +1,29 @@ package com.github.codehive.model.dto; +import java.util.UUID; + import com.fasterxml.jackson.annotation.JsonInclude; import com.github.codehive.model.enums.Language; @JsonInclude(JsonInclude.Include.NON_NULL) public class ReferenceSolutionDTO { - private Long id; - private Long assignmentId; + private UUID id; + private UUID assignmentId; private Language language; - public Long getId() { + public UUID getId() { return id; } - public void setId(Long id) { + public void setId(UUID id) { this.id = id; } - public Long getAssignmentId() { + public UUID getAssignmentId() { return assignmentId; } - public void setAssignmentId(Long assignmentId) { + public void setAssignmentId(UUID assignmentId) { this.assignmentId = assignmentId; } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/SubmissionDTO.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/SubmissionDTO.java index 3f21a16..f3df4ff 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/dto/SubmissionDTO.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/SubmissionDTO.java @@ -1,30 +1,31 @@ package com.github.codehive.model.dto; import java.time.LocalDateTime; +import java.util.UUID; import com.fasterxml.jackson.annotation.JsonInclude; import com.github.codehive.model.enums.Language; @JsonInclude(JsonInclude.Include.NON_NULL) public class SubmissionDTO { - private Long id; - private Long assignmentId; + private UUID id; + private UUID assignmentId; private Language language; private LocalDateTime createdAt; - public Long getId() { + public UUID getId() { return id; } - public void setId(Long id) { + public void setId(UUID id) { this.id = id; } - public Long getAssignmentId() { + public UUID getAssignmentId() { return assignmentId; } - public void setAssignmentId(Long assignmentId) { + public void setAssignmentId(UUID assignmentId) { this.assignmentId = assignmentId; } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/TestCaseDTO.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/TestCaseDTO.java index 7063a28..47ee5ba 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/dto/TestCaseDTO.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/TestCaseDTO.java @@ -1,30 +1,31 @@ package com.github.codehive.model.dto; import java.time.LocalDateTime; +import java.util.UUID; import com.fasterxml.jackson.annotation.JsonInclude; @JsonInclude(JsonInclude.Include.NON_NULL) public class TestCaseDTO { - private Long id; - private Long assignmentId; + private UUID id; + private UUID assignmentId; private Integer order; private Boolean isSample; private LocalDateTime createdAt; - public Long getId() { + public UUID getId() { return id; } - public void setId(Long id) { + public void setId(UUID id) { this.id = id; } - public Long getAssignmentId() { + public UUID getAssignmentId() { return assignmentId; } - public void setAssignmentId(Long assignmentId) { + public void setAssignmentId(UUID assignmentId) { this.assignmentId = assignmentId; } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/UserDTO.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/UserDTO.java index abc58ba..d1f6d1d 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/dto/UserDTO.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/UserDTO.java @@ -2,6 +2,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.UUID; import com.fasterxml.jackson.annotation.JsonInclude; import com.github.codehive.model.enums.Role; @@ -9,7 +10,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class UserDTO { - private Long id; + private UUID id; private String email; private String name; private String lastName; @@ -21,11 +22,11 @@ public class UserDTO { private Boolean isActive; private Boolean temporaryPassword; - public Long getId() { + public UUID getId() { return id; } - public void setId(Long id) { + public void setId(UUID id) { this.id = id; } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/ExecutionJob.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/ExecutionJob.java index ff199d7..e46fffe 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/ExecutionJob.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/ExecutionJob.java @@ -1,6 +1,7 @@ package com.github.codehive.model.dto.queue; import java.util.List; +import java.util.UUID; import com.github.codehive.model.enums.ComparatorType; import com.github.codehive.model.enums.ExecutionType; @@ -8,7 +9,7 @@ public class ExecutionJob { - private Long id; + private UUID id; private String source; private String reference; private Language language; @@ -25,8 +26,8 @@ public class ExecutionJob { public ExecutionJob() { } - public ExecutionJob(Long id, String source, String reference, Language language, - ExecutionType executionType, List testCases, + public ExecutionJob(UUID id, String source, String reference, Language language, + ExecutionType executionType, List testCases, Long timeLimitMs, Long memoryLimitMb, ComparatorType comparatorType, String outputPath, Integer numTests, String testsPath, Language referenceLanguage) { this.id = id; this.source = source; @@ -74,11 +75,11 @@ public void setOutputPath(String outputPath) { this.outputPath = outputPath; } - public Long getId() { + public UUID getId() { return id; } - public void setId(Long id) { + public void setId(UUID id) { this.id = id; } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/ExecutionReport.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/ExecutionReport.java index 460c6ab..96361fb 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/ExecutionReport.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/ExecutionReport.java @@ -1,12 +1,14 @@ package com.github.codehive.model.dto.queue; +import java.util.UUID; + import com.github.codehive.model.enums.ExecutionStatus; import java.util.ArrayList; import java.util.List; public class ExecutionReport { - private Long executionId; + private UUID executionId; private ExecutionStatus overallStatus; private List testCaseResults; private int totalTests; @@ -21,16 +23,16 @@ public ExecutionReport() { this.testCaseResults = new ArrayList<>(); } - public ExecutionReport(Long executionId) { + public ExecutionReport(UUID executionId) { this.executionId = executionId; this.testCaseResults = new ArrayList<>(); } - public Long getExecutionId() { + public UUID getExecutionId() { return executionId; } - public void setExecutionId(Long executionId) { + public void setExecutionId(UUID executionId) { this.executionId = executionId; } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/entity/Assignment.java b/codehive-backend/src/main/java/com/github/codehive/model/entity/Assignment.java index 63afdf1..c65d89a 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/entity/Assignment.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/entity/Assignment.java @@ -3,6 +3,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import com.github.codehive.model.enums.ComparatorType; import com.github.codehive.model.enums.Language; @@ -23,8 +24,8 @@ @Table(name = "assignments") public class Assignment { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; @Column(nullable = false, length = 200) private String title; @@ -110,11 +111,11 @@ public void setAllowedLanguages(List allowedLanguages) { this.allowedLanguages = allowedLanguages; } - public Long getId() { + public UUID getId() { return id; } - public void setId(Long id) { + public void setId(UUID id) { this.id = id; } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/entity/Execution.java b/codehive-backend/src/main/java/com/github/codehive/model/entity/Execution.java index b1cbda8..c1dc9e9 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/entity/Execution.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/entity/Execution.java @@ -1,6 +1,7 @@ package com.github.codehive.model.entity; import java.time.LocalDateTime; +import java.util.UUID; import com.github.codehive.model.enums.ExecutionStatus; import com.github.codehive.model.enums.ExecutionType; @@ -22,8 +23,8 @@ @Table(name = "executions") public class Execution { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "submission_id", nullable = true, unique = true) @@ -76,11 +77,11 @@ public Execution(ExecutionType executionType, User user) { this.user = user; } - public Long getId() { + public UUID getId() { return id; } - public void setId(Long id) { + public void setId(UUID id) { this.id = id; } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/entity/PasswordResetToken.java b/codehive-backend/src/main/java/com/github/codehive/model/entity/PasswordResetToken.java index c6b24f4..195713b 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/entity/PasswordResetToken.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/entity/PasswordResetToken.java @@ -1,6 +1,7 @@ package com.github.codehive.model.entity; import java.time.LocalDateTime; +import java.util.UUID; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -15,8 +16,8 @@ @Table(name = "password_reset_tokens") public class PasswordResetToken { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; @Column(nullable = false, unique = true) private String token; @@ -41,11 +42,11 @@ public PasswordResetToken(String token, LocalDateTime expiryDate, User user) { this.used = false; } - public Long getId() { + public UUID getId() { return id; } - public void setId(Long id) { + public void setId(UUID id) { this.id = id; } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/entity/ReferenceSolution.java b/codehive-backend/src/main/java/com/github/codehive/model/entity/ReferenceSolution.java index 6989e5d..83db33c 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/entity/ReferenceSolution.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/entity/ReferenceSolution.java @@ -1,5 +1,7 @@ package com.github.codehive.model.entity; +import java.util.UUID; + import com.github.codehive.model.enums.Language; import jakarta.persistence.Column; @@ -18,8 +20,8 @@ @Table(name = "reference_solutions") public class ReferenceSolution { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "assignment_id", nullable = false) @@ -37,11 +39,11 @@ public ReferenceSolution(Assignment assignment, Language language) { this.language = language; } - public Long getId() { + public UUID getId() { return id; } - public void setId(Long id) { + public void setId(UUID id) { this.id = id; } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/entity/Submission.java b/codehive-backend/src/main/java/com/github/codehive/model/entity/Submission.java index 8525e31..4610ba0 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/entity/Submission.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/entity/Submission.java @@ -1,6 +1,7 @@ package com.github.codehive.model.entity; import java.time.LocalDateTime; +import java.util.UUID; import com.github.codehive.model.enums.Language; @@ -20,8 +21,8 @@ @Table(name = "submissions") public class Submission { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "assignment_id", nullable = false) @@ -44,11 +45,11 @@ public Submission(Assignment assignment, Language language) { this.language = language; } - public Long getId() { + public UUID getId() { return id; } - public void setId(Long id) { + public void setId(UUID id) { this.id = id; } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/entity/TestCase.java b/codehive-backend/src/main/java/com/github/codehive/model/entity/TestCase.java index c558bad..c19b3dc 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/entity/TestCase.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/entity/TestCase.java @@ -1,5 +1,7 @@ package com.github.codehive.model.entity; +import java.util.UUID; + import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -14,8 +16,8 @@ @Table(name = "test_cases") public class TestCase { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "assignment_id", nullable = false) @@ -38,11 +40,11 @@ public TestCase(Assignment assignment, Integer order, Boolean isSample) { this.isSample = isSample; } - public Long getId() { + public UUID getId() { return id; } - public void setId(Long id) { + public void setId(UUID id) { this.id = id; } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/entity/User.java b/codehive-backend/src/main/java/com/github/codehive/model/entity/User.java index 26edabf..a68fa78 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/entity/User.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/entity/User.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.UUID; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -28,8 +29,8 @@ @Table(name = "users") public class User implements UserDetails { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; @Column(nullable = false, length = 50) private String name; @@ -85,11 +86,11 @@ public User(String name, String lastName, String enrollmentNumber, String email, this.scopes = new ArrayList<>(); } - public Long getId() { + public UUID getId() { return id; } - public void setId(Long id) { + public void setId(UUID id) { this.id = id; } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/request/execution/ExecutionRequest.java b/codehive-backend/src/main/java/com/github/codehive/model/request/execution/ExecutionRequest.java index 2044afd..b48aa98 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/request/execution/ExecutionRequest.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/request/execution/ExecutionRequest.java @@ -1,6 +1,7 @@ package com.github.codehive.model.request.execution; import java.util.List; +import java.util.UUID; import com.github.codehive.model.enums.ExecutionType; import com.github.codehive.model.enums.Language; @@ -15,9 +16,9 @@ public class ExecutionRequest { @NotNull(message = "Language is required") private Language language; - private Long requesterId; - - private Long assignmentId; + private UUID requesterId; + + private UUID assignmentId; private List testCases; @@ -27,8 +28,8 @@ public class ExecutionRequest { public ExecutionRequest() { } - public ExecutionRequest(String code, Language language, Long requesterId, - Long assignmentId, List testCases, ExecutionType executionType) { + public ExecutionRequest(String code, Language language, UUID requesterId, + UUID assignmentId, List testCases, ExecutionType executionType) { this.code = code; this.language = language; this.requesterId = requesterId; @@ -53,19 +54,19 @@ public void setLanguage(Language language) { this.language = language; } - public Long getRequesterId() { + public UUID getRequesterId() { return requesterId; } - public void setRequesterId(Long requesterId) { + public void setRequesterId(UUID requesterId) { this.requesterId = requesterId; } - public Long getAssignmentId() { + public UUID getAssignmentId() { return assignmentId; } - public void setAssignmentId(Long assignmentId) { + public void setAssignmentId(UUID assignmentId) { this.assignmentId = assignmentId; } diff --git a/codehive-backend/src/main/java/com/github/codehive/repository/AssignmentRepository.java b/codehive-backend/src/main/java/com/github/codehive/repository/AssignmentRepository.java index 3b728c6..2b6fb38 100644 --- a/codehive-backend/src/main/java/com/github/codehive/repository/AssignmentRepository.java +++ b/codehive-backend/src/main/java/com/github/codehive/repository/AssignmentRepository.java @@ -3,12 +3,13 @@ import java.time.LocalDateTime; import java.util.List; import java.util.Optional; +import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; import com.github.codehive.model.entity.Assignment; -public interface AssignmentRepository extends JpaRepository { +public interface AssignmentRepository extends JpaRepository { List findAllByOrderByCreatedAtDesc(); List findByTitleContainingIgnoreCase(String title); @@ -17,5 +18,5 @@ public interface AssignmentRepository extends JpaRepository { List findByDueDateAfter(LocalDateTime date); - Optional findByIdAndDueDateAfter(Long id, LocalDateTime date); + Optional findByIdAndDueDateAfter(UUID id, LocalDateTime date); } diff --git a/codehive-backend/src/main/java/com/github/codehive/repository/ExecutionRepository.java b/codehive-backend/src/main/java/com/github/codehive/repository/ExecutionRepository.java index 11b7d4a..23765a5 100644 --- a/codehive-backend/src/main/java/com/github/codehive/repository/ExecutionRepository.java +++ b/codehive-backend/src/main/java/com/github/codehive/repository/ExecutionRepository.java @@ -2,6 +2,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; @@ -9,7 +10,7 @@ import com.github.codehive.model.enums.ExecutionStatus; import com.github.codehive.model.enums.ExecutionType; -public interface ExecutionRepository extends JpaRepository { +public interface ExecutionRepository extends JpaRepository { List findByExecutionType(ExecutionType executionType); List findByStatus(ExecutionStatus status); diff --git a/codehive-backend/src/main/java/com/github/codehive/repository/PasswordResetTokenRepository.java b/codehive-backend/src/main/java/com/github/codehive/repository/PasswordResetTokenRepository.java index 1b45bc6..0bf651b 100644 --- a/codehive-backend/src/main/java/com/github/codehive/repository/PasswordResetTokenRepository.java +++ b/codehive-backend/src/main/java/com/github/codehive/repository/PasswordResetTokenRepository.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Optional; +import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -10,7 +11,7 @@ import com.github.codehive.model.entity.User; @Repository -public interface PasswordResetTokenRepository extends JpaRepository { +public interface PasswordResetTokenRepository extends JpaRepository { Optional findByToken(String token); List findByUserAndUsedFalse(User user); diff --git a/codehive-backend/src/main/java/com/github/codehive/repository/ReferenceSolutionRepository.java b/codehive-backend/src/main/java/com/github/codehive/repository/ReferenceSolutionRepository.java index a080fcf..59870af 100644 --- a/codehive-backend/src/main/java/com/github/codehive/repository/ReferenceSolutionRepository.java +++ b/codehive-backend/src/main/java/com/github/codehive/repository/ReferenceSolutionRepository.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Optional; +import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; @@ -9,12 +10,12 @@ import com.github.codehive.model.entity.ReferenceSolution; import com.github.codehive.model.enums.Language; -public interface ReferenceSolutionRepository extends JpaRepository { +public interface ReferenceSolutionRepository extends JpaRepository { List findByAssignment(Assignment assignment); Optional findByAssignmentAndLanguage(Assignment assignment, Language language); - List findByAssignmentId(Long assignmentId); + List findByAssignmentId(UUID assignmentId); boolean existsByAssignmentAndLanguage(Assignment assignment, Language language); } diff --git a/codehive-backend/src/main/java/com/github/codehive/repository/SubmissionRepository.java b/codehive-backend/src/main/java/com/github/codehive/repository/SubmissionRepository.java index 1175ed8..d03a116 100644 --- a/codehive-backend/src/main/java/com/github/codehive/repository/SubmissionRepository.java +++ b/codehive-backend/src/main/java/com/github/codehive/repository/SubmissionRepository.java @@ -2,22 +2,23 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; import com.github.codehive.model.entity.Assignment; import com.github.codehive.model.entity.Submission; -public interface SubmissionRepository extends JpaRepository { +public interface SubmissionRepository extends JpaRepository { List findByAssignment(Assignment assignment); - List findByAssignmentId(Long assignmentId); - - List findByAssignmentIdOrderByCreatedAtDesc(Long assignmentId); + List findByAssignmentId(UUID assignmentId); + + List findByAssignmentIdOrderByCreatedAtDesc(UUID assignmentId); List findByCreatedAtBefore(LocalDateTime date); List findByCreatedAtAfter(LocalDateTime date); - long countByAssignmentId(Long assignmentId); + long countByAssignmentId(UUID assignmentId); } diff --git a/codehive-backend/src/main/java/com/github/codehive/repository/TestCaseRepository.java b/codehive-backend/src/main/java/com/github/codehive/repository/TestCaseRepository.java index c8c9e72..732ba05 100644 --- a/codehive-backend/src/main/java/com/github/codehive/repository/TestCaseRepository.java +++ b/codehive-backend/src/main/java/com/github/codehive/repository/TestCaseRepository.java @@ -1,24 +1,25 @@ package com.github.codehive.repository; import java.util.List; +import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; import com.github.codehive.model.entity.Assignment; import com.github.codehive.model.entity.TestCase; -public interface TestCaseRepository extends JpaRepository { +public interface TestCaseRepository extends JpaRepository { List findByAssignment(Assignment assignment); - List findByAssignmentId(Long assignmentId); - + List findByAssignmentId(UUID assignmentId); + List findByAssignmentOrderByOrderAsc(Assignment assignment); - - List findByAssignmentIdOrderByOrderAsc(Long assignmentId); - + + List findByAssignmentIdOrderByOrderAsc(UUID assignmentId); + List findByAssignmentAndIsSample(Assignment assignment, Boolean isSample); - - List findByAssignmentIdAndIsSample(Long assignmentId, Boolean isSample); - - long countByAssignmentId(Long assignmentId); + + List findByAssignmentIdAndIsSample(UUID assignmentId, Boolean isSample); + + long countByAssignmentId(UUID assignmentId); } diff --git a/codehive-backend/src/main/java/com/github/codehive/repository/UserRepository.java b/codehive-backend/src/main/java/com/github/codehive/repository/UserRepository.java index f5c6ed1..4ec2d14 100644 --- a/codehive-backend/src/main/java/com/github/codehive/repository/UserRepository.java +++ b/codehive-backend/src/main/java/com/github/codehive/repository/UserRepository.java @@ -2,12 +2,13 @@ import java.util.List; import java.util.Optional; +import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; import com.github.codehive.model.entity.User; -public interface UserRepository extends JpaRepository { +public interface UserRepository extends JpaRepository { Optional findByEmail(String email); Optional findByEnrollmentNumber(String enrollmentNumber); diff --git a/codehive-backend/src/main/java/com/github/codehive/service/AuthService.java b/codehive-backend/src/main/java/com/github/codehive/service/AuthService.java index a7ddef3..df079c3 100644 --- a/codehive-backend/src/main/java/com/github/codehive/service/AuthService.java +++ b/codehive-backend/src/main/java/com/github/codehive/service/AuthService.java @@ -11,6 +11,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.UUID; import java.util.regex.Pattern; import org.apache.commons.csv.CSVFormat; @@ -230,7 +231,7 @@ private String generateToken(User user) { } @Transactional - public void updatePassword(Long userId, com.github.codehive.model.request.auth.UpdatePasswordRequest request) { + public void updatePassword(UUID userId, com.github.codehive.model.request.auth.UpdatePasswordRequest request) { User user = userRepository.findById(userId) .orElseThrow(() -> new IncorrectCredentialsException("User not found")); diff --git a/codehive-backend/src/main/java/com/github/codehive/service/ExecutionRequestService.java b/codehive-backend/src/main/java/com/github/codehive/service/ExecutionRequestService.java index a560979..73a59e5 100644 --- a/codehive-backend/src/main/java/com/github/codehive/service/ExecutionRequestService.java +++ b/codehive-backend/src/main/java/com/github/codehive/service/ExecutionRequestService.java @@ -1,5 +1,7 @@ package com.github.codehive.service; +import java.util.UUID; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -93,7 +95,7 @@ public ExecutionDTO requestExecution(ExecutionRequest request) { } @Transactional(readOnly = true) - public ExecutionDTO getExecutionById(Long id) { + public ExecutionDTO getExecutionById(UUID id) { Execution execution = executionRepository.findById(id) .orElseThrow(() -> new EntityNotFoundException("Execution not found with id: " + id)); return ExecutionMapper.toDTO(execution); diff --git a/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java b/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java index ce63e2a..e87fc7c 100644 --- a/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java +++ b/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java @@ -1,36 +1,37 @@ package com.github.codehive.utils; +import java.util.UUID; + public class ObjectKeyBuilder { - public static String testCaseInput(Long assignmentId, Long testCaseId) { - return String.format("test-suites/assignments/%d/tc-%d/tc%d.in", assignmentId, testCaseId, testCaseId); + public static String testCaseInput(UUID assignmentId, UUID testCaseId) { + return String.format("test-suites/assignments/%s/tc-%s/tc%s.in", assignmentId, testCaseId, testCaseId); } - public static String testCaseOutput(Long assignmentId, Long testCaseId) { - return String.format("test-suites/assignments/%d/tc-%d/tc%d.out", assignmentId, testCaseId, testCaseId); + public static String testCaseOutput(UUID assignmentId, UUID testCaseId) { + return String.format("test-suites/assignments/%s/tc-%s/tc%s.out", assignmentId, testCaseId, testCaseId); } - public static String testsPath(Long assignmentId) { - return String.format("test-suites/assignments/%d/", assignmentId); - + public static String testsPath(UUID assignmentId) { + return String.format("test-suites/assignments/%s/", assignmentId); } - public static String submissionSourceCode(Long assignmentId, Long submissionId, String fileExtension, Long groupId) { - return String.format("submissions/groups/%d/assignments/%d/submission-%d/Main.%s", groupId, assignmentId, submissionId, fileExtension); + public static String submissionSourceCode(UUID assignmentId, UUID submissionId, String fileExtension, UUID groupId) { + return String.format("submissions/groups/%s/assignments/%s/submission-%s/Main.%s", groupId, assignmentId, submissionId, fileExtension); } - public static String executionOutput(Long submissionId, Long executionId, Long groupId, String fileExtension) { - return String.format("executions/groups/%d/assignments/%d/execution-%d/output.%s", groupId, submissionId, executionId, fileExtension); + public static String executionOutput(UUID submissionId, UUID executionId, UUID groupId, String fileExtension) { + return String.format("executions/groups/%s/assignments/%s/execution-%s/output.%s", groupId, submissionId, executionId, fileExtension); } - public static String referenceSolutionSourceCode(Long assignmentId, String fileExtension) { - return String.format("test-suites/assignments/%d/reference/Main.%s", assignmentId, fileExtension); + public static String referenceSolutionSourceCode(UUID assignmentId, String fileExtension) { + return String.format("test-suites/assignments/%s/reference/Main.%s", assignmentId, fileExtension); } - public static String executionTestCaseOutput(Long executionId) { - return String.format("test-execution/execution-%d/output/", executionId); + public static String executionTestCaseOutput(UUID executionId) { + return String.format("test-execution/execution-%s/output/", executionId); } - public static String executionSourceCode(Long executionId, String fileExtension) { - return String.format("test-execution/execution-%d/source.%s", executionId, fileExtension); + public static String executionSourceCode(UUID executionId, String fileExtension) { + return String.format("test-execution/execution-%s/source.%s", executionId, fileExtension); } } diff --git a/codehive-backend/src/main/java/com/github/codehive/websocket/CsvProgressWebSocketHandler.java b/codehive-backend/src/main/java/com/github/codehive/websocket/CsvProgressWebSocketHandler.java index 5eef471..8b66dad 100644 --- a/codehive-backend/src/main/java/com/github/codehive/websocket/CsvProgressWebSocketHandler.java +++ b/codehive-backend/src/main/java/com/github/codehive/websocket/CsvProgressWebSocketHandler.java @@ -9,6 +9,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; @@ -38,7 +39,7 @@ public CsvProgressWebSocketHandler(ObjectMapper objectMapper) { } @Override - protected void handleTextMessage(WebSocketSession session, TextMessage message) { + protected void handleTextMessage(@NonNull WebSocketSession session, @NonNull TextMessage message) { String taskId = message.getPayload().trim(); session.getAttributes().put(TASK_ID_ATTRIBUTE, taskId); taskSessions.put(taskId, session); @@ -58,7 +59,7 @@ public void queueTask(String taskId, Runnable task) { } @Override - public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { + public void afterConnectionClosed(@NonNull WebSocketSession session, @NonNull CloseStatus status) { String taskId = (String) session.getAttributes().get(TASK_ID_ATTRIBUTE); if (taskId != null) { taskSessions.remove(taskId); diff --git a/codehive-backend/src/test/java/com/github/codehive/model/mapper/UserMapperTest.java b/codehive-backend/src/test/java/com/github/codehive/model/mapper/UserMapperTest.java index 275ff03..23a3248 100644 --- a/codehive-backend/src/test/java/com/github/codehive/model/mapper/UserMapperTest.java +++ b/codehive-backend/src/test/java/com/github/codehive/model/mapper/UserMapperTest.java @@ -5,6 +5,7 @@ import java.time.LocalDateTime; import java.util.Arrays; import java.util.List; +import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -18,6 +19,13 @@ @DisplayName("UserMapper Unit") class UserMapperTest { + private static final UUID USER_ID_1 = UUID.fromString("00000000-0000-0000-0000-000000000001"); + private static final UUID USER_ID_2 = UUID.fromString("00000000-0000-0000-0000-000000000002"); + private static final UUID USER_ID_A = UUID.fromString("00000000-0000-0000-0000-000000000010"); + private static final UUID USER_ID_B = UUID.fromString("00000000-0000-0000-0000-000000000020"); + private static final UUID MINIMAL_ID = UUID.fromString("00000000-0000-0000-0000-000000000999"); + private static final UUID DTO_MIN_ID = UUID.fromString("00000000-0000-0000-0000-000000000888"); + private User testUser; private UserDTO testUserDTO; private LocalDateTime testDate; @@ -28,7 +36,7 @@ void setUp() { // Setup test user entity testUser = new User(); - testUser.setId(1L); + testUser.setId(USER_ID_1); testUser.setEmail("test@example.com"); testUser.setName("John"); testUser.setLastName("Doe"); @@ -41,7 +49,7 @@ void setUp() { // Setup test user DTO testUserDTO = new UserDTO(); - testUserDTO.setId(2L); + testUserDTO.setId(USER_ID_2); testUserDTO.setEmail("jane@example.com"); testUserDTO.setName("Jane"); testUserDTO.setLastName("Smith"); @@ -90,7 +98,7 @@ void toDTO_WithNullUser_ReturnsNull() { void toDTO_WithMinimalUser_MapsAvailableFields() { // Given User minimalUser = new User(); - minimalUser.setId(999L); + minimalUser.setId(MINIMAL_ID); minimalUser.setEmail("minimal@example.com"); // When @@ -98,7 +106,7 @@ void toDTO_WithMinimalUser_MapsAvailableFields() { // Then assertThat(result).isNotNull(); - assertThat(result.getId()).isEqualTo(999L); + assertThat(result.getId()).isEqualTo(MINIMAL_ID); assertThat(result.getEmail()).isEqualTo("minimal@example.com"); assertThat(result.getName()).isNull(); assertThat(result.getLastName()).isNull(); @@ -176,7 +184,7 @@ void toEntity_WithNullDTO_ReturnsNull() { void toEntity_WithMinimalDTO_MapsAvailableFields() { // Given UserDTO minimalDTO = new UserDTO(); - minimalDTO.setId(888L); + minimalDTO.setId(DTO_MIN_ID); minimalDTO.setEmail("minimal@example.com"); // When @@ -184,7 +192,7 @@ void toEntity_WithMinimalDTO_MapsAvailableFields() { // Then assertThat(result).isNotNull(); - assertThat(result.getId()).isEqualTo(888L); + assertThat(result.getId()).isEqualTo(DTO_MIN_ID); assertThat(result.getEmail()).isEqualTo("minimal@example.com"); assertThat(result.getName()).isNull(); assertThat(result.getPassword()).isNull(); @@ -200,13 +208,13 @@ class ListMappingTests { void toDTOList_WithMultipleUsers_MapsAllCorrectly() { // Given User user1 = new User(); - user1.setId(1L); + user1.setId(USER_ID_1); user1.setEmail("user1@example.com"); user1.setName("User"); user1.setLastName("One"); - + User user2 = new User(); - user2.setId(2L); + user2.setId(USER_ID_2); user2.setEmail("user2@example.com"); user2.setName("User"); user2.setLastName("Two"); @@ -218,9 +226,9 @@ void toDTOList_WithMultipleUsers_MapsAllCorrectly() { // Then assertThat(result).hasSize(2); - assertThat(result.get(0).getId()).isEqualTo(1L); + assertThat(result.get(0).getId()).isEqualTo(USER_ID_1); assertThat(result.get(0).getEmail()).isEqualTo("user1@example.com"); - assertThat(result.get(1).getId()).isEqualTo(2L); + assertThat(result.get(1).getId()).isEqualTo(USER_ID_2); assertThat(result.get(1).getEmail()).isEqualTo("user2@example.com"); } @@ -239,11 +247,11 @@ void toDTOList_WithEmptyList_ReturnsEmptyList() { void toEntityList_WithMultipleDTOs_MapsAllCorrectly() { // Given UserDTO dto1 = new UserDTO(); - dto1.setId(10L); + dto1.setId(USER_ID_A); dto1.setEmail("dto1@example.com"); - + UserDTO dto2 = new UserDTO(); - dto2.setId(20L); + dto2.setId(USER_ID_B); dto2.setEmail("dto2@example.com"); List dtos = Arrays.asList(dto1, dto2); @@ -253,9 +261,9 @@ void toEntityList_WithMultipleDTOs_MapsAllCorrectly() { // Then assertThat(result).hasSize(2); - assertThat(result.get(0).getId()).isEqualTo(10L); + assertThat(result.get(0).getId()).isEqualTo(USER_ID_A); assertThat(result.get(0).getEmail()).isEqualTo("dto1@example.com"); - assertThat(result.get(1).getId()).isEqualTo(20L); + assertThat(result.get(1).getId()).isEqualTo(USER_ID_B); assertThat(result.get(1).getEmail()).isEqualTo("dto2@example.com"); } @@ -264,7 +272,7 @@ void toEntityList_WithMultipleDTOs_MapsAllCorrectly() { void toDTOList_WithNullValuesInList_SkipsNulls() { // Given User user1 = new User(); - user1.setId(1L); + user1.setId(USER_ID_1); user1.setEmail("user1@example.com"); List users = Arrays.asList(user1, null); diff --git a/codehive-backend/src/test/java/com/github/codehive/service/AuthServiceTest.java b/codehive-backend/src/test/java/com/github/codehive/service/AuthServiceTest.java index 6cebfee..2973d99 100644 --- a/codehive-backend/src/test/java/com/github/codehive/service/AuthServiceTest.java +++ b/codehive-backend/src/test/java/com/github/codehive/service/AuthServiceTest.java @@ -15,6 +15,7 @@ import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Optional; +import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -68,7 +69,7 @@ class AuthServiceTest { void setUp() { // Setup test user testUser = new User(); - testUser.setId(1L); + testUser.setId(UUID.fromString("00000000-0000-0000-0000-000000000001")); testUser.setEmail("test@example.com"); testUser.setPassword("encodedPassword123"); testUser.setName("John"); @@ -162,7 +163,7 @@ void login_GeneratesTokenWithCorrectClaims() { when(passwordEncoder.matches(loginRequest.getPassword(), testUser.getPassword())).thenReturn(true); when(jwtUtil.generateToken(any(Map.class), anyString())).thenAnswer(invocation -> { Map claims = invocation.getArgument(0); - assertThat(claims).containsEntry("userId", 1L); + assertThat(claims).containsEntry("userId", testUser.getId()); assertThat(claims).containsEntry("role", "STUDENT"); return "jwt-token-123"; }); @@ -213,7 +214,7 @@ void register_WithValidData_ReturnsUserDTO() { when(passwordEncoder.encode(anyString())).thenReturn("encodedPassword"); User savedUser = new User(); - savedUser.setId(2L); + savedUser.setId(UUID.fromString("00000000-0000-0000-0000-000000000002")); savedUser.setEmail(signUpRequest.getEmail()); savedUser.setPassword("encodedPassword"); savedUser.setName(signUpRequest.getName()); @@ -285,7 +286,7 @@ void register_GeneratesAndEncodesRandomPassword() { when(passwordEncoder.encode(anyString())).thenReturn("encodedRandomPassword"); User savedUser = new User(); - savedUser.setId(2L); + savedUser.setId(UUID.fromString("00000000-0000-0000-0000-000000000002")); savedUser.setEmail(signUpRequest.getEmail()); savedUser.setName(signUpRequest.getName()); savedUser.setLastName("Smith Doe"); @@ -313,7 +314,7 @@ void register_SetsRoleFromRequest() { when(passwordEncoder.encode(anyString())).thenReturn("encodedPassword"); User savedUser = new User(); - savedUser.setId(2L); + savedUser.setId(UUID.fromString("00000000-0000-0000-0000-000000000002")); savedUser.setEmail(signUpRequest.getEmail()); savedUser.setName(signUpRequest.getName()); savedUser.setLastName("Smith Doe"); @@ -343,7 +344,7 @@ void register_SetsUserAsActiveByDefault() { when(passwordEncoder.encode(anyString())).thenReturn("encodedPassword"); User savedUser = new User(); - savedUser.setId(2L); + savedUser.setId(UUID.fromString("00000000-0000-0000-0000-000000000002")); savedUser.setEmail(signUpRequest.getEmail()); savedUser.setName(signUpRequest.getName()); savedUser.setLastName("Smith Doe"); @@ -374,7 +375,7 @@ void register_SetsTemporaryPasswordTrue() { when(passwordEncoder.encode(anyString())).thenReturn("encodedPassword"); User savedUser = new User(); - savedUser.setId(2L); + savedUser.setId(UUID.fromString("00000000-0000-0000-0000-000000000002")); savedUser.setEmail(signUpRequest.getEmail()); savedUser.setName(signUpRequest.getName()); savedUser.setLastName("Smith Doe"); @@ -404,7 +405,7 @@ void register_SendsWelcomeEmailWithTemporaryPassword() { when(passwordEncoder.encode(anyString())).thenReturn("encodedPassword"); User savedUser = new User(); - savedUser.setId(2L); + savedUser.setId(UUID.fromString("00000000-0000-0000-0000-000000000002")); savedUser.setEmail(signUpRequest.getEmail()); savedUser.setName(signUpRequest.getName()); savedUser.setLastName("Smith Doe"); @@ -432,7 +433,7 @@ void register_ConcatenatesLastNames() { ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); User savedUser = new User(); - savedUser.setId(2L); + savedUser.setId(UUID.fromString("00000000-0000-0000-0000-000000000002")); savedUser.setEmail(signUpRequest.getEmail()); savedUser.setName(signUpRequest.getName()); savedUser.setLastName("Smith Doe"); diff --git a/codehive-backend/src/test/java/com/github/codehive/service/RecoveryPasswordServiceTest.java b/codehive-backend/src/test/java/com/github/codehive/service/RecoveryPasswordServiceTest.java index 9f1d840..bba3c5f 100644 --- a/codehive-backend/src/test/java/com/github/codehive/service/RecoveryPasswordServiceTest.java +++ b/codehive-backend/src/test/java/com/github/codehive/service/RecoveryPasswordServiceTest.java @@ -14,6 +14,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Optional; +import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -61,7 +62,7 @@ class RecoveryPasswordServiceTest { void setUp() { // Setup test user testUser = new User(); - testUser.setId(1L); + testUser.setId(UUID.fromString("00000000-0000-0000-0000-000000000001")); testUser.setEmail("test@example.com"); testUser.setPassword("oldEncodedPassword"); testUser.setName("John"); @@ -72,7 +73,7 @@ void setUp() { // Setup valid token validToken = new PasswordResetToken(); - validToken.setId(1L); + validToken.setId(UUID.fromString("00000000-0000-0000-0000-000000000011")); validToken.setToken("valid-token-123"); validToken.setExpiryDate(LocalDateTime.now().plusMinutes(10)); validToken.setUsed(false); @@ -189,12 +190,12 @@ void sendPasswordResetEmail_InvalidatesOldUnusedTokens() { String email = "test@example.com"; PasswordResetToken oldToken1 = new PasswordResetToken(); - oldToken1.setId(10L); + oldToken1.setId(UUID.fromString("00000000-0000-0000-0000-000000000020")); oldToken1.setToken("old-token-1"); oldToken1.setUsed(false); - + PasswordResetToken oldToken2 = new PasswordResetToken(); - oldToken2.setId(11L); + oldToken2.setId(UUID.fromString("00000000-0000-0000-0000-000000000021")); oldToken2.setToken("old-token-2"); oldToken2.setUsed(false); diff --git a/codehive-frontend/app/shared/types/model/User.ts b/codehive-frontend/app/shared/types/model/User.ts index 2b8fafb..fe5f63d 100644 --- a/codehive-frontend/app/shared/types/model/User.ts +++ b/codehive-frontend/app/shared/types/model/User.ts @@ -13,7 +13,7 @@ export enum Scope { } export interface User { - id: number; + id: string; email: string; name: string; lastName: string; diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionReport.java b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionReport.java index 8c60c1d..86a235a 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionReport.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionReport.java @@ -1,12 +1,13 @@ package com.github.codehive.worker.model.dto; -import com.github.codehive.worker.model.enums.ExecutionStatus; - import java.util.ArrayList; +import java.util.UUID; + +import com.github.codehive.worker.model.enums.ExecutionStatus; import java.util.List; public class ExecutionReport { - private Long executionId; + private UUID executionId; private ExecutionStatus overallStatus; private List testCaseResults; private int totalTests; @@ -21,7 +22,7 @@ public ExecutionReport() { this.testCaseResults = new ArrayList<>(); } - public ExecutionReport(Long executionId) { + public ExecutionReport(UUID executionId) { this.executionId = executionId; this.testCaseResults = new ArrayList<>(); } @@ -93,11 +94,11 @@ public void determineOverallStatus() { } // Getters and setters - public Long getExecutionId() { + public UUID getExecutionId() { return executionId; } - public void setExecutionId(Long executionId) { + public void setExecutionId(UUID executionId) { this.executionId = executionId; } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/queue/ExecutionJob.java b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/queue/ExecutionJob.java index 65ec0e4..fcfd245 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/queue/ExecutionJob.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/queue/ExecutionJob.java @@ -1,13 +1,14 @@ package com.github.codehive.worker.model.dto.queue; import java.util.List; +import java.util.UUID; import com.github.codehive.worker.model.enums.ComparatorType; import com.github.codehive.worker.model.enums.ExecutionType; import com.github.codehive.worker.model.enums.Language; public class ExecutionJob { - private Long id; + private UUID id; private String source; private String reference; private Language language; @@ -24,8 +25,8 @@ public class ExecutionJob { public ExecutionJob() { } - public ExecutionJob(Long id, String source, String reference, Language language, - ExecutionType executionType, List testCases, + public ExecutionJob(UUID id, String source, String reference, Language language, + ExecutionType executionType, List testCases, Long timeLimitMs, Long memoryLimitMb, ComparatorType comparatorType, String outputPath, Integer numTests, String testsPath, Language referenceLanguage) { this.id = id; this.source = source; @@ -73,11 +74,11 @@ public void setOutputPath(String outputPath) { this.outputPath = outputPath; } - public Long getId() { + public UUID getId() { return id; } - public void setId(Long id) { + public void setId(UUID id) { this.id = id; } From 2a85db2c0e75286346d9de06eabbf3e7b707c896 Mon Sep 17 00:00:00 2001 From: IrminDev Date: Fri, 8 May 2026 22:33:22 -0600 Subject: [PATCH 15/32] Updated assignment logic, updated documentation --- AGENTS.md | 81 +++- README.md | 395 +++++++++++------- .../github/codehive/config/RabbitConfig.java | 12 + .../controller/AssignmentController.java | 88 ++++ .../TestGenerationResultListener.java | 44 ++ .../TestGenerationRequestProducer.java | 34 ++ .../model/dto/queue/TestCaseInfo.java | 42 ++ .../model/dto/queue/TestGenerationJob.java | 76 ++++ .../model/dto/queue/TestGenerationResult.java | 52 +++ .../assignment/CreateAssignmentRequest.java | 144 +++++++ .../codehive/service/AssignmentService.java | 127 ++++++ .../service/ExecutionRequestService.java | 146 +++---- .../codehive/utils/ObjectKeyBuilder.java | 8 +- .../worker/config/RabbitMQConfig.java | 12 + .../TestGenerationRequestListener.java | 43 ++ .../TestGenerationResultProducer.java | 26 ++ .../worker/model/dto/queue/TestCaseInfo.java | 42 ++ .../model/dto/queue/TestGenerationJob.java | 66 +++ .../model/dto/queue/TestGenerationResult.java | 52 +++ .../worker/service/TestGenerationService.java | 96 +++++ llms/backend/OVERVIEW.md | 46 +- llms/backend/controller/README.md | 30 +- llms/backend/executions/README.md | 115 +++-- llms/backend/messaging/README.md | 71 ++-- llms/backend/model/README.md | 131 +++--- llms/backend/service/README.md | 52 ++- llms/worker/OVERVIEW.md | 57 ++- llms/worker/execution/README.md | 82 ++-- llms/worker/messaging/README.md | 99 ++--- 29 files changed, 1720 insertions(+), 549 deletions(-) create mode 100644 codehive-backend/src/main/java/com/github/codehive/controller/AssignmentController.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/messaging/listener/TestGenerationResultListener.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/messaging/producer/TestGenerationRequestProducer.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/dto/queue/TestCaseInfo.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/dto/queue/TestGenerationJob.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/dto/queue/TestGenerationResult.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/model/request/assignment/CreateAssignmentRequest.java create mode 100644 codehive-backend/src/main/java/com/github/codehive/service/AssignmentService.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/TestGenerationRequestListener.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/messaging/producer/TestGenerationResultProducer.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/model/dto/queue/TestCaseInfo.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/model/dto/queue/TestGenerationJob.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/model/dto/queue/TestGenerationResult.java create mode 100644 codehive-worker/src/main/java/com/github/codehive/worker/service/TestGenerationService.java diff --git a/AGENTS.md b/AGENTS.md index 6ff2bfc..bacd269 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,11 +42,11 @@ llms/ │ ├── services/ │ └── student/ └── worker/ - ├── OVERVIEW.md - ├── comparator/ - ├── execution/ - ├── messaging/ - └── sandbox/ + ├── OVERVIEW.md + ├── comparator/ + ├── execution/ + ├── messaging/ + └── sandbox/ ``` ## Quick Doc Lookup @@ -78,10 +78,10 @@ General prerequisites: - Docker and Docker Compose Typical local flow: -1. Start infrastructure (PostgreSQL, RabbitMQ, and other required services) with Docker Compose from backend. -2. Run backend from codehive-backend. -3. Run worker from codehive-worker when execution pipelines are needed. -4. Run frontend from codehive-frontend. +1. Start infrastructure (PostgreSQL, RabbitMQ, MinIO) with Docker Compose from codehive-backend. +2. Run backend from codehive-backend (`./gradlew bootRun`). +3. Run worker from codehive-worker when execution pipelines are needed (`./gradlew bootRun`). +4. Run frontend from codehive-frontend (`npm run dev`). Use project wrappers and local scripts: - Backend and worker: ./gradlew @@ -89,13 +89,14 @@ Use project wrappers and local scripts: ## Testing Run tests in each project independently: -- Backend: unit and integration tests via Gradle -- Worker: service and sandbox-related tests via Gradle -- Frontend: type checks and UI/app tests via npm scripts +- Backend: unit and integration tests via Gradle (`./gradlew test`) +- Worker: service and sandbox-related tests via Gradle (`./gradlew test`) +- Frontend: type checks and UI/app tests via npm scripts (`npm run typecheck`) Testing rules: - Add or update tests for every behavior change. - Keep tests isolated and deterministic. +- Use fixed `UUID.fromString("00000000-0000-0000-0000-000000000001")` values in tests — not `UUID.randomUUID()`. - Prefer small unit tests plus focused integration coverage. ## CI/CD @@ -124,7 +125,14 @@ High-level architecture: - Frontend consumes backend REST APIs. - Backend handles auth, core business logic, persistence, and messaging orchestration. - Worker consumes execution jobs, runs sandboxed code, and publishes execution results. -- Shared infrastructure includes RabbitMQ and object storage. +- Shared infrastructure includes RabbitMQ, PostgreSQL, and MinIO (object storage). + +For concrete architecture, contracts, data models, and execution flow, always consult: +- llms/backend/OVERVIEW.md +- llms/frontend/OVERVIEW.md +- llms/worker/OVERVIEW.md + +Then drill into corresponding llms subfolders for implementation details. ## ID Convention All entity primary keys use `java.util.UUID` (not `Long`). @@ -134,12 +142,47 @@ All entity primary keys use `java.util.UUID` (not `Long`). - Controllers: `@PathVariable UUID id` - Frontend: entity IDs are typed as `string` -For concrete architecture, contracts, data models, and execution flow, always consult: -- llms/backend/OVERVIEW.md -- llms/frontend/OVERVIEW.md -- llms/worker/OVERVIEW.md - -Then drill into corresponding llms subfolders for implementation details. +## Queue Topology +Four durable queues — names are configurable via system properties, defaults shown: + +| Queue | Direction | Purpose | +|---|---|---| +| `codehive_queue` | backend → worker | Student code execution jobs | +| `codehive_result_queue` | worker → backend | Execution results | +| `codehive_test_generation_queue` | backend → worker | Teacher assignment creation — generate expected outputs | +| `codehive_test_generation_result_queue` | worker → backend | Output generation result | + +Payload contracts: +- `model/dto/queue/ExecutionJob` — student execution job +- `model/dto/queue/ExecutionReport` — student execution result +- `model/dto/queue/TestGenerationJob` — output generation job (contains list of `TestCaseInfo`) +- `model/dto/queue/TestGenerationResult` — output generation outcome + +Queue DTO changes are contract changes — coordinate backend and worker deployment together. + +## MinIO Object Key Conventions +All paths are produced by `utils/ObjectKeyBuilder`: + +| Method | Path pattern | +|---|---| +| `testCaseInput(assignmentId, testCaseId)` | `test-suites/assignments/{a}/tc-{t}/tc{t}.in` | +| `testCaseOutput(assignmentId, testCaseId)` | `test-suites/assignments/{a}/tc-{t}/tc{t}.out` | +| `testsPath(assignmentId)` | `test-suites/assignments/{a}/` | +| `referenceSolutionSourceCode(assignmentId, ext)` | `test-suites/assignments/{a}/reference/Main.{ext}` | +| `executionSourceCode(executionId, ext)` | `test-execution/execution-{e}/source.{ext}` | +| `executionTestCaseOutput(executionId)` | `test-execution/execution-{e}/output/` | +| `submissionSourceCode(assignmentId, submissionId, ext)` | `submissions/assignments/{a}/submission-{s}/Main.{ext}` | + +## Implemented Features +- Authentication: login, signup (admin), CSV bulk signup with WebSocket progress +- Password recovery: forgot/reset flow with 15-minute tokens +- Assignment creation: teacher uploads reference solution + test case inputs; worker generates expected outputs asynchronously +- Student execution: PRACTICE (inline test cases vs reference solution) and DEFINITIVE (pre-generated outputs) +- Execution status polling + +## Not Yet Implemented (frontend) +- Professor/teacher pages for assignment management +- Student pages for submitting code and viewing results ## Documentation Routing Rule When working on a specific area, read docs in this order: diff --git a/README.md b/README.md index 166d3ec..1b8d184 100644 --- a/README.md +++ b/README.md @@ -1,224 +1,307 @@ -# CodeHive 🐝 - -### 📌 Status Badge - -Already included at the top of README: -```markdown [![Backend CI](https://github.com/IrminDev/CodeHive/actions/workflows/backend-ci.yml/badge.svg)](https://github.com/IrminDev/CodeHive/actions/workflows/backend-ci.yml) -``` [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Java](https://img.shields.io/badge/Java-21-orange.svg)](https://www.oracle.com/java/) -[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.5.6-brightgreen.svg)](https://spring.io/projects/spring-boot) +[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.x-brightgreen.svg)](https://spring.io/projects/spring-boot) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org/) -> A collaborative platform for educational purposes that allows teachers to create groups with their students, and students can deliver code by editing it from the same platform. +# CodeHive -## 🚀 Features +> A collaborative platform for programming education. Teachers create assignments with automated test generation; students submit code that is evaluated in sandboxed Docker containers. -- 🔐 **Secure Authentication** - JWT-based authentication with password recovery -- 👥 **User Management** - Role-based access control (Students, Teachers, Admins) -- 🛡️ **Rate Limiting** - Protection against bot attacks and brute force -- 📧 **Email Integration** - Password reset and notifications -- 📝 **API Documentation** - Interactive Swagger/OpenAPI documentation -- ✅ **Comprehensive Testing** - 121 tests with 70%+ code coverage -- 🔄 **CI/CD** - Automated testing and deployment pipelines +## Features -## 📋 Table of Contents +- **Secure Authentication** — JWT-based login, password recovery, role-based access (Student, Teacher, Admin) +- **Bulk User Registration** — CSV upload with real-time WebSocket progress streaming +- **Assignment Management** — Teachers upload reference solutions and test case inputs; expected outputs are generated automatically by the worker +- **Sandboxed Code Execution** — Submissions run in isolated Docker containers with CPU, memory, and time limits +- **Multi-language Support** — Java, Python, C, C++ +- **Two Execution Modes** — Practice (inline test cases vs reference solution) and Definitive (pre-generated expected outputs) +- **Output Comparison** — Exact match and floating-point comparators +- **Asynchronous Pipeline** — Execution and test generation fully decoupled via RabbitMQ +- **Object Storage** — Source code, test inputs, expected outputs, and execution artifacts stored in MinIO +- **API Documentation** — Interactive Swagger/OpenAPI at `/swagger-ui.html` +- **Rate Limiting** — Per-endpoint throttling to prevent abuse +- **CI/CD** — Automated testing and coverage via GitHub Actions -- [Getting Started](#getting-started) -- [Architecture](#architecture) -- [Testing](#testing) -- [API Documentation](#api-documentation) -- [Development](#development) -- [Contributing](#contributing) +## Architecture -## 🏁 Getting Started +``` +┌─────────────┐ REST API ┌──────────────────────────────────────────────┐ +│ Frontend │ ◄────────────► │ Backend │ +│ React/TS │ │ Spring Boot · PostgreSQL · MinIO │ +└─────────────┘ │ │ + │ ┌──────────┐ ┌───────────────────────┐ │ + │ │ Auth │ │ Assignment Service │ │ + │ │ Service │ │ (upload + queue job) │ │ + │ └──────────┘ └───────────┬───────────┘ │ + │ ┌──────────────────────┐ │ │ + │ │ Execution Service │ │ │ + │ │ (load assignment, │ │ │ + │ │ queue job) │ │ │ + │ └──────────┬───────────┘ │ │ + └─────────────┼──────────────┼────────────────┘ + │ RabbitMQ │ + ┌─────────────▼──────────────▼────────────────┐ + │ Worker │ + │ Spring Boot · Docker SDK · MinIO │ + │ │ + │ ┌──────────────────┐ ┌─────────────────┐ │ + │ │ TestExecutionSvc │ │ TestGenerationSvc│ │ + │ │ (run submission) │ │ (gen outputs) │ │ + │ └──────────────────┘ └─────────────────┘ │ + └─────────────────────────────────────────────┘ +``` -### Prerequisites +### Queue Topology -- **Java 21** or later -- **PostgreSQL 15+** (for production) -- **Node.js 18+** (for frontend) -- **Gradle** (included via wrapper) +| Queue | Direction | Purpose | +|---|---|---| +| `codehive_queue` | backend → worker | Student code execution jobs | +| `codehive_result_queue` | worker → backend | Execution results | +| `codehive_test_generation_queue` | backend → worker | Generate expected test outputs | +| `codehive_test_generation_result_queue` | worker → backend | Output generation result | -### Backend Setup +### MinIO Object Layout -1. **Clone the repository** - ```bash - git clone https://github.com/IrminDev/CodeHive.git - cd CodeHive/codehive-backend - ``` +``` +test-suites/assignments/{assignmentId}/ + reference/Main.{ext} ← reference solution + tc-{testCaseId}/tc{testCaseId}.in ← test case input + tc-{testCaseId}/tc{testCaseId}.out ← expected output (worker-generated) -2. **Configure environment variables** - ```bash - cp .env.example .env - # Edit .env with your configuration - ``` +test-execution/execution-{executionId}/ + source.{ext} ← submitted code + output/tc-{n}/stdout.txt ← actual output + output/tc-{n}/stderr.txt -3. **Run the application** - ```bash - ./gradlew bootRun - ``` +submissions/assignments/{assignmentId}/submission-{id}/Main.{ext} +``` + +## Tech Stack + +| Layer | Technology | +|---|---| +| Backend API | Spring Boot 3, Java 21, Spring Security, JPA/Hibernate | +| Frontend | React Router v7, TypeScript, Vite | +| Worker | Spring Boot 3, Java 21, Docker Java SDK | +| Database | PostgreSQL | +| Message Broker | RabbitMQ | +| Object Storage | MinIO | +| Auth | JWT (stateless), BCrypt | +| Testing | JUnit 5, Mockito, AssertJ | +| Docs | SpringDoc OpenAPI / Swagger | + +## Getting Started + +### Prerequisites -4. **Access the application** - - API: http://localhost:8080 - - Swagger UI: http://localhost:8080/swagger-ui.html +- Java 21+ +- Node.js 18+ +- Docker and Docker Compose -### Frontend Setup +### 1. Start Infrastructure ```bash -cd codehive-frontend -npm install -npm run dev +cd codehive-backend +docker compose up -d ``` -Access at: http://localhost:3000 +This starts PostgreSQL, RabbitMQ, and MinIO. -## 🏗️ Architecture +### 2. Backend -### Backend Stack +```bash +cd codehive-backend +./gradlew bootRun +``` -- **Framework**: Spring Boot 3.5.6 -- **Language**: Java 21 (LTS) -- **Database**: PostgreSQL (production), H2 (testing) -- **Security**: Spring Security + JWT -- **Rate Limiting**: Bucket4j -- **Email**: JavaMailSender -- **Testing**: JUnit 5, Mockito, AssertJ -- **Documentation**: SpringDoc OpenAPI +Available at: http://localhost:8080 +Swagger UI: http://localhost:8080/swagger-ui.html -### Project Structure +### 3. Worker -``` -codehive-backend/ -├── src/main/java/com/github/codehive/ -│ ├── config/ # Configuration classes -│ ├── controller/ # REST controllers -│ ├── model/ # Entities, DTOs, requests, responses -│ ├── repository/ # JPA repositories -│ ├── service/ # Business logic -│ ├── security/ # Security configuration -│ ├── ratelimit/ # Rate limiting -│ └── utils/ # Utility classes -└── src/test/java/ # Tests (unit & integration) +```bash +cd codehive-worker +./gradlew bootRun ``` -## ✅ Testing +The worker connects to RabbitMQ and MinIO on startup and begins consuming jobs. -### Running Tests +### 4. Frontend ```bash -# All tests -./gradlew test +cd codehive-frontend +npm install +npm run dev +``` -# Unit tests only -./gradlew test --tests "*Test" --exclude-tests "*IntegrationTest" +Available at: http://localhost:3000 -# Integration tests only -./gradlew test --tests "*IntegrationTest" +## API Reference -# With coverage report -./gradlew test jacocoTestReport -``` +### Authentication -### Test Coverage +| Method | Endpoint | Auth | Description | +|---|---|---|---| +| POST | `/api/auth/login` | — | Login with email or enrollment number | +| POST | `/api/auth/signup` | ADMIN | Create a single user account | +| POST | `/api/auth/signup/csv` | ADMIN | Bulk register users from CSV | +| GET | `/api/auth/me` | Bearer | Get current authenticated user | -- **121 total tests** - - 71 unit tests - - 50 integration tests -- **70%+ code coverage** -- **Test pyramid followed**: 70% unit, 30% integration +### Password Recovery -### Coverage Report +| Method | Endpoint | Auth | Description | +|---|---|---|---| +| POST | `/api/recovery-password/forgot` | — | Request a password reset email | +| POST | `/api/recovery-password/reset` | — | Reset password with token | -View the detailed coverage report: -```bash -open build/reports/jacoco/test/html/index.html -``` +### Assignments -## 📚 API Documentation +| Method | Endpoint | Auth | Description | +|---|---|---|---| +| POST | `/api/assignments` | TEACHER / ADMIN | Create assignment with reference solution and test inputs | -### Interactive Documentation +The assignment endpoint accepts `multipart/form-data` with three parts: +- `metadata` — JSON with title, description, limits, comparator, languages, sampleFlags +- `referenceSolution` — source file +- `testCaseInputs` — one or more input files (order determines test case index) -Access Swagger UI at: http://localhost:8080/swagger-ui.html +The assignment is created as inactive. The worker runs the reference solution against each input and stores the expected outputs. Once complete the assignment is automatically activated. -### Main Endpoints +### Code Execution -#### Authentication -- `POST /api/auth/login` - User login -- `POST /api/auth/signup` - User registration +| Method | Endpoint | Auth | Description | +|---|---|---|---| +| POST | `/api/execution/check` | — | Submit code for execution | +| GET | `/api/execution/check/{id}` | — | Poll execution status | -#### Password Recovery -- `POST /api/recovery-password/forgot` - Request password reset -- `POST /api/recovery-password/reset` - Reset password with token +Execution request body: -### Rate Limits +```json +{ + "code": "...", + "language": "JAVA", + "executionType": "PRACTICE", + "assignmentId": "uuid", + "requesterId": "uuid", + "testCases": ["input1", "input2"] +} +``` -| Endpoint | Limit | Duration | -|----------|-------|----------| -| Login | 5 requests | 60 seconds | -| Signup | 3 requests | 5 minutes | -| Forgot Password | 3 requests | 5 minutes | -| Reset Password | 5 requests | 5 minutes | +`PRACTICE` — inline test cases are compared against the reference solution output. +`DEFINITIVE` — submission is compared against pre-generated expected outputs from MinIO. -## 🛠️ Development +### Rate Limits -### Code Style +| Endpoint | Limit | Window | +|---|---|---| +| POST `/api/auth/login` | 5 requests | 60 s | +| POST `/api/auth/signup` | 3 requests | 5 min | +| POST `/api/recovery-password/forgot` | 3 requests | 5 min | +| POST `/api/recovery-password/reset` | 5 requests | 5 min | +| POST `/api/execution/check` | 10 requests | 60 s | -- Follow Java naming conventions -- Use meaningful variable/method names -- Add JavaDoc for public APIs -- Keep methods focused and small +## Execution Verdict Reference -### Git Workflow +| Status | Meaning | +|---|---| +| `PENDING` | Queued, not yet processed | +| `AC` | Accepted — all test cases passed | +| `WA` | Wrong Answer | +| `CE` | Compilation Error | +| `RTE` | Runtime Error | +| `TLE` | Time Limit Exceeded | +| `MLE` | Memory Limit Exceeded | -1. Create a feature branch: `git checkout -b feature/my-feature` -2. Make your changes and commit: `git commit -am 'Add new feature'` -3. Push to the branch: `git push origin feature/my-feature` -4. Create a Pull Request +Overall verdict priority when tests fail: `CE > TLE > MLE > RTE > WA`. -### CI/CD Pipeline +## Supported Languages -Every push and PR triggers: -- ✅ Automated testing (unit & integration) -- 📊 Code coverage analysis -- 🏗️ Application build -- 📝 Test results published to PR +| Language | Compile command | Run command | +|---|---|---| +| Java | `javac Main.java` | `java Main` | +| Python | — | `python main.py` | +| C | `gcc -o program main.c -lm` | `./program` | +| C++ | `g++ -o program main.cpp -std=c++17 -lm` | `./program` | -See [.github/workflows/README.md](.github/workflows/README.md) for details. +## Testing -## 🤝 Contributing +```bash +# Backend unit and integration tests +cd codehive-backend +./gradlew test -We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details. +# With coverage report +./gradlew test jacocoTestReport +open build/reports/jacoco/test/html/index.html -### Quick Start for Contributors +# Worker tests +cd codehive-worker +./gradlew test -1. Fork the repository -2. Create your feature branch -3. Write tests for your changes -4. Ensure all tests pass: `./gradlew test` -5. Commit your changes -6. Push to your fork -7. Create a Pull Request +# Frontend type check +cd codehive-frontend +npm run typecheck +``` -## 📝 License +## Project Structure -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +``` +CodeHive/ +├── codehive-backend/ ← Spring Boot REST API +│ ├── src/main/java/com/github/codehive/ +│ │ ├── config/ ← RabbitMQ, MinIO, Security, WebSocket, Async +│ │ ├── controller/ ← Auth, RecoveryPassword, Assignment, CheckExecution +│ │ ├── messaging/ ← Producers and listeners for both queue pairs +│ │ ├── model/ ← Entities, DTOs, queue DTOs, requests, responses +│ │ ├── repository/ ← JPA repositories (UUID primary keys) +│ │ ├── security/ ← JWT filter, UserDetailsService +│ │ ├── service/ ← Auth, Assignment, Execution, ObjectStorage, Mail +│ │ ├── ratelimit/ ← @RateLimit annotation and aspect +│ │ ├── websocket/ ← CSV progress handler +│ │ └── utils/ ← ObjectKeyBuilder, FileExtensionUtil, JwtUtil +│ └── src/test/ ← Unit and integration tests +│ +├── codehive-worker/ ← Sandboxed execution worker +│ └── src/main/java/com/github/codehive/worker/ +│ ├── config/ ← Docker client, MinIO, RabbitMQ +│ ├── messaging/ ← Listeners (execution + generation) and producers +│ ├── model/ ← DTOs and enums +│ ├── sandbox/ ← LanguageExecutor interface and implementations +│ └── service/ ← TestExecutionService, TestGenerationService +│ +├── codehive-frontend/ ← React Router v7 SPA +│ └── app/ +│ ├── components/ ← Reusable UI and ProtectedRoute guard +│ ├── context/ ← Auth and theme providers +│ ├── pages/ ← Admin, login, recovery pages +│ ├── routes/ ← Route entry files +│ ├── services/ ← AuthService, RecoveryPasswordService +│ └── types/ ← TypeScript contracts +│ +└── llms/ ← Implementation documentation for AI agents + ├── backend/ + ├── frontend/ + └── worker/ +``` + +## Contributing -## 👥 Authors +1. Fork the repository +2. Create a feature branch: `git checkout -b feature/my-feature` +3. Write tests for your changes +4. Ensure all tests pass +5. Open a Pull Request against `main` -- **Irmin** - *Initial work* - [IrminDev](https://github.com/IrminDev) +## License -## 🙏 Acknowledgments +MIT — see [LICENSE](LICENSE) for details. -- Spring Boot team for the excellent framework -- All contributors who have helped this project +## Authors -## 📞 Support +- **Irmin Hernandez Jimenez** — [IrminDev](https://github.com/IrminDev) +- **Johann Daniel Trejo Flores** — [JohannTF](https://github.com/JohannTF) +- **Rodolfo Aparicio Lopez** — [rodolfo-rgb ](https://github.com/rodolfo-rgb) -- 📧 Email: irmin@codehive.com -- 🐛 Issues: [GitHub Issues](https://github.com/IrminDev/CodeHive/issues) -- 💬 Discussions: [GitHub Discussions](https://github.com/IrminDev/CodeHive/discussions) --- - -Made with ❤️ for education diff --git a/codehive-backend/src/main/java/com/github/codehive/config/RabbitConfig.java b/codehive-backend/src/main/java/com/github/codehive/config/RabbitConfig.java index 226e0b4..694fda4 100644 --- a/codehive-backend/src/main/java/com/github/codehive/config/RabbitConfig.java +++ b/codehive-backend/src/main/java/com/github/codehive/config/RabbitConfig.java @@ -10,6 +10,8 @@ public class RabbitConfig { public static final String QUEUE_NAME = System.getProperty("rabbitmq.queue", "codehive_queue"); public static final String RESULT_QUEUE_NAME = System.getProperty("rabbitmq.result.queue", "codehive_result_queue"); + public static final String TEST_GENERATION_QUEUE_NAME = System.getProperty("rabbitmq.test-generation.queue", "codehive_test_generation_queue"); + public static final String TEST_GENERATION_RESULT_QUEUE_NAME = System.getProperty("rabbitmq.test-generation.result.queue", "codehive_test_generation_result_queue"); @Bean Queue executionQueue() { @@ -21,6 +23,16 @@ Queue resultQueue() { return new Queue(RESULT_QUEUE_NAME, true); } + @Bean + Queue testGenerationQueue() { + return new Queue(TEST_GENERATION_QUEUE_NAME, true); + } + + @Bean + Queue testGenerationResultQueue() { + return new Queue(TEST_GENERATION_RESULT_QUEUE_NAME, true); + } + @Bean public MessageConverter jsonMessageConverter() { return new Jackson2JsonMessageConverter(); diff --git a/codehive-backend/src/main/java/com/github/codehive/controller/AssignmentController.java b/codehive-backend/src/main/java/com/github/codehive/controller/AssignmentController.java new file mode 100644 index 0000000..ce8ffa0 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/controller/AssignmentController.java @@ -0,0 +1,88 @@ +package com.github.codehive.controller; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import com.github.codehive.model.dto.AssignmentDTO; +import com.github.codehive.model.request.assignment.CreateAssignmentRequest; +import com.github.codehive.model.response.ErrorResponse; +import com.github.codehive.model.response.SuccessResponse; +import com.github.codehive.service.AssignmentService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; + +@RestController +@RequestMapping("/api/assignments") +@Tag(name = "Assignments", description = "Assignment management APIs") +public class AssignmentController { + private static final Logger logger = LoggerFactory.getLogger(AssignmentController.class); + + private final AssignmentService assignmentService; + + public AssignmentController(AssignmentService assignmentService) { + this.assignmentService = assignmentService; + } + + @Operation( + summary = "Create a new assignment", + description = "Teacher uploads assignment metadata, a reference solution, and test case input files. " + + "The assignment is created as inactive. The worker runs the reference solution against each " + + "input to generate expected outputs, then activates the assignment." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "202", + description = "Assignment created and test generation queued", + content = @Content(schema = @Schema(implementation = SuccessResponse.class)) + ), + @ApiResponse( + responseCode = "400", + description = "Validation error or missing files", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ), + @ApiResponse( + responseCode = "403", + description = "Forbidden — only TEACHER or ADMIN role allowed", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ) + }) + @PreAuthorize("hasAnyRole('TEACHER', 'ADMIN')") + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity> createAssignment( + @Valid @RequestPart("metadata") CreateAssignmentRequest metadata, + @RequestPart("referenceSolution") MultipartFile referenceSolution, + @RequestPart("testCaseInputs") List testCaseInputs) { + + logger.info("POST /api/assignments - title={}, language={}, testCases={}", + metadata.getTitle(), metadata.getReferenceLanguage(), testCaseInputs.size()); + + if (testCaseInputs.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + + AssignmentDTO created = assignmentService.createAssignment(metadata, referenceSolution, testCaseInputs); + + logger.info("Assignment queued for test generation: id={}", created.getId()); + + return ResponseEntity + .status(HttpStatus.ACCEPTED) + .body(new SuccessResponse<>("Assignment created. Test output generation in progress.", created)); + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/messaging/listener/TestGenerationResultListener.java b/codehive-backend/src/main/java/com/github/codehive/messaging/listener/TestGenerationResultListener.java new file mode 100644 index 0000000..b666773 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/messaging/listener/TestGenerationResultListener.java @@ -0,0 +1,44 @@ +package com.github.codehive.messaging.listener; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +import com.github.codehive.model.dto.queue.TestGenerationResult; +import com.github.codehive.model.entity.Assignment; +import com.github.codehive.repository.AssignmentRepository; + +@Component +public class TestGenerationResultListener { + private static final Logger logger = LoggerFactory.getLogger(TestGenerationResultListener.class); + + private final AssignmentRepository assignmentRepository; + + public TestGenerationResultListener(AssignmentRepository assignmentRepository) { + this.assignmentRepository = assignmentRepository; + } + + @RabbitListener(queues = "${rabbitmq.test-generation.result.queue:codehive_test_generation_result_queue}") + public void handleTestGenerationResult(TestGenerationResult result) { + logger.info("[WORKFLOW] RABBITMQ RECEIVE: Test generation result - assignmentId={}, success={}, generated={}", + result.getAssignmentId(), result.isSuccess(), result.getGeneratedCount()); + + Assignment assignment = assignmentRepository.findById(result.getAssignmentId()).orElse(null); + if (assignment == null) { + logger.warn("[WORKFLOW] Assignment not found for test generation result - assignmentId={}", + result.getAssignmentId()); + return; + } + + if (result.isSuccess()) { + assignment.setIsActive(true); + assignmentRepository.save(assignment); + logger.info("[WORKFLOW] Assignment activated after test generation - assignmentId={}, generatedOutputs={}", + assignment.getId(), result.getGeneratedCount()); + } else { + logger.error("[WORKFLOW] Test generation failed for assignmentId={}, error={}", + result.getAssignmentId(), result.getErrorMessage()); + } + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/messaging/producer/TestGenerationRequestProducer.java b/codehive-backend/src/main/java/com/github/codehive/messaging/producer/TestGenerationRequestProducer.java new file mode 100644 index 0000000..a49e5b5 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/messaging/producer/TestGenerationRequestProducer.java @@ -0,0 +1,34 @@ +package com.github.codehive.messaging.producer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.stereotype.Service; + +import com.github.codehive.config.RabbitConfig; +import com.github.codehive.model.dto.queue.TestGenerationJob; + +@Service +public class TestGenerationRequestProducer { + private static final Logger logger = LoggerFactory.getLogger(TestGenerationRequestProducer.class); + + private final RabbitTemplate rabbitTemplate; + + public TestGenerationRequestProducer(RabbitTemplate rabbitTemplate) { + this.rabbitTemplate = rabbitTemplate; + } + + public void sendTestGenerationRequest(TestGenerationJob job) { + logger.info("[WORKFLOW] RABBITMQ SEND: Sending test generation job - assignmentId={}, testCases={}, language={}", + job.getAssignmentId(), job.getTestCases().size(), job.getReferenceLanguage()); + try { + rabbitTemplate.convertAndSend(RabbitConfig.TEST_GENERATION_QUEUE_NAME, job); + logger.info("[WORKFLOW] RABBITMQ SEND: Test generation job queued successfully - assignmentId={}", + job.getAssignmentId()); + } catch (Exception e) { + logger.error("[WORKFLOW] RABBITMQ SEND FAILED: Could not queue test generation job - assignmentId={}, error={}", + job.getAssignmentId(), e.getMessage(), e); + throw e; + } + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/TestCaseInfo.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/TestCaseInfo.java new file mode 100644 index 0000000..7687fd8 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/TestCaseInfo.java @@ -0,0 +1,42 @@ +package com.github.codehive.model.dto.queue; + +import java.util.UUID; + +public class TestCaseInfo { + private UUID testCaseId; + private String inputPath; + private String outputPath; + + public TestCaseInfo() { + } + + public TestCaseInfo(UUID testCaseId, String inputPath, String outputPath) { + this.testCaseId = testCaseId; + this.inputPath = inputPath; + this.outputPath = outputPath; + } + + public UUID getTestCaseId() { + return testCaseId; + } + + public void setTestCaseId(UUID testCaseId) { + this.testCaseId = testCaseId; + } + + public String getInputPath() { + return inputPath; + } + + public void setInputPath(String inputPath) { + this.inputPath = inputPath; + } + + public String getOutputPath() { + return outputPath; + } + + public void setOutputPath(String outputPath) { + this.outputPath = outputPath; + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/TestGenerationJob.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/TestGenerationJob.java new file mode 100644 index 0000000..cb848ef --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/TestGenerationJob.java @@ -0,0 +1,76 @@ +package com.github.codehive.model.dto.queue; + +import java.util.List; +import java.util.UUID; + +import com.github.codehive.model.enums.Language; + +public class TestGenerationJob { + private UUID assignmentId; + private String referenceSolutionPath; + private Language referenceLanguage; + private List testCases; + private Long timeLimitMs; + private Long memoryLimitMb; + + public TestGenerationJob() { + } + + public TestGenerationJob(UUID assignmentId, String referenceSolutionPath, Language referenceLanguage, + List testCases, Long timeLimitMs, Long memoryLimitMb) { + this.assignmentId = assignmentId; + this.referenceSolutionPath = referenceSolutionPath; + this.referenceLanguage = referenceLanguage; + this.testCases = testCases; + this.timeLimitMs = timeLimitMs; + this.memoryLimitMb = memoryLimitMb; + } + + public UUID getAssignmentId() { + return assignmentId; + } + + public void setAssignmentId(UUID assignmentId) { + this.assignmentId = assignmentId; + } + + public String getReferenceSolutionPath() { + return referenceSolutionPath; + } + + public void setReferenceSolutionPath(String referenceSolutionPath) { + this.referenceSolutionPath = referenceSolutionPath; + } + + public Language getReferenceLanguage() { + return referenceLanguage; + } + + public void setReferenceLanguage(Language referenceLanguage) { + this.referenceLanguage = referenceLanguage; + } + + public List getTestCases() { + return testCases; + } + + public void setTestCases(List testCases) { + this.testCases = testCases; + } + + public Long getTimeLimitMs() { + return timeLimitMs; + } + + public void setTimeLimitMs(Long timeLimitMs) { + this.timeLimitMs = timeLimitMs; + } + + public Long getMemoryLimitMb() { + return memoryLimitMb; + } + + public void setMemoryLimitMb(Long memoryLimitMb) { + this.memoryLimitMb = memoryLimitMb; + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/TestGenerationResult.java b/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/TestGenerationResult.java new file mode 100644 index 0000000..00e729f --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/dto/queue/TestGenerationResult.java @@ -0,0 +1,52 @@ +package com.github.codehive.model.dto.queue; + +import java.util.UUID; + +public class TestGenerationResult { + private UUID assignmentId; + private boolean success; + private int generatedCount; + private String errorMessage; + + public TestGenerationResult() { + } + + public TestGenerationResult(UUID assignmentId, boolean success, int generatedCount, String errorMessage) { + this.assignmentId = assignmentId; + this.success = success; + this.generatedCount = generatedCount; + this.errorMessage = errorMessage; + } + + public UUID getAssignmentId() { + return assignmentId; + } + + public void setAssignmentId(UUID assignmentId) { + this.assignmentId = assignmentId; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public int getGeneratedCount() { + return generatedCount; + } + + public void setGeneratedCount(int generatedCount) { + this.generatedCount = generatedCount; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/model/request/assignment/CreateAssignmentRequest.java b/codehive-backend/src/main/java/com/github/codehive/model/request/assignment/CreateAssignmentRequest.java new file mode 100644 index 0000000..3216ce6 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/model/request/assignment/CreateAssignmentRequest.java @@ -0,0 +1,144 @@ +package com.github.codehive.model.request.assignment; + +import java.time.LocalDateTime; +import java.util.List; + +import com.github.codehive.model.enums.ComparatorType; +import com.github.codehive.model.enums.Language; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +public class CreateAssignmentRequest { + + @NotBlank(message = "Title is required") + private String title; + + @NotBlank(message = "Description is required") + private String description; + + private List constraints; + private List hints; + private List tags; + + @NotNull(message = "Time limit is required") + @Min(value = 100, message = "Time limit must be at least 100ms") + private Long timeLimitMs; + + @NotNull(message = "Memory limit is required") + @Min(value = 16, message = "Memory limit must be at least 16MB") + private Long memoryLimitMb; + + @NotNull(message = "Comparator type is required") + private ComparatorType comparatorType; + + @NotEmpty(message = "At least one allowed language is required") + private List allowedLanguages; + + @NotNull(message = "Reference language is required") + private Language referenceLanguage; + + private LocalDateTime dueDate; + + // Parallel list indicating whether each uploaded test case input is a sample. + // If null or shorter than the number of uploaded files, remaining cases default to non-sample. + private List sampleFlags; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getConstraints() { + return constraints; + } + + public void setConstraints(List constraints) { + this.constraints = constraints; + } + + public List getHints() { + return hints; + } + + public void setHints(List hints) { + this.hints = hints; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } + + public Long getTimeLimitMs() { + return timeLimitMs; + } + + public void setTimeLimitMs(Long timeLimitMs) { + this.timeLimitMs = timeLimitMs; + } + + public Long getMemoryLimitMb() { + return memoryLimitMb; + } + + public void setMemoryLimitMb(Long memoryLimitMb) { + this.memoryLimitMb = memoryLimitMb; + } + + public ComparatorType getComparatorType() { + return comparatorType; + } + + public void setComparatorType(ComparatorType comparatorType) { + this.comparatorType = comparatorType; + } + + public List getAllowedLanguages() { + return allowedLanguages; + } + + public void setAllowedLanguages(List allowedLanguages) { + this.allowedLanguages = allowedLanguages; + } + + public Language getReferenceLanguage() { + return referenceLanguage; + } + + public void setReferenceLanguage(Language referenceLanguage) { + this.referenceLanguage = referenceLanguage; + } + + public LocalDateTime getDueDate() { + return dueDate; + } + + public void setDueDate(LocalDateTime dueDate) { + this.dueDate = dueDate; + } + + public List getSampleFlags() { + return sampleFlags; + } + + public void setSampleFlags(List sampleFlags) { + this.sampleFlags = sampleFlags; + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/service/AssignmentService.java b/codehive-backend/src/main/java/com/github/codehive/service/AssignmentService.java new file mode 100644 index 0000000..594a8d6 --- /dev/null +++ b/codehive-backend/src/main/java/com/github/codehive/service/AssignmentService.java @@ -0,0 +1,127 @@ +package com.github.codehive.service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import com.github.codehive.messaging.producer.TestGenerationRequestProducer; +import com.github.codehive.model.dto.AssignmentDTO; +import com.github.codehive.model.dto.queue.TestCaseInfo; +import com.github.codehive.model.dto.queue.TestGenerationJob; +import com.github.codehive.model.entity.Assignment; +import com.github.codehive.model.entity.ReferenceSolution; +import com.github.codehive.model.entity.TestCase; +import com.github.codehive.model.mapper.AssignmentMapper; +import com.github.codehive.model.request.assignment.CreateAssignmentRequest; +import com.github.codehive.repository.AssignmentRepository; +import com.github.codehive.repository.ReferenceSolutionRepository; +import com.github.codehive.repository.TestCaseRepository; +import com.github.codehive.utils.FileExtensionUtil; +import com.github.codehive.utils.ObjectKeyBuilder; + +@Service +public class AssignmentService { + private static final Logger logger = LoggerFactory.getLogger(AssignmentService.class); + + private final AssignmentRepository assignmentRepository; + private final TestCaseRepository testCaseRepository; + private final ReferenceSolutionRepository referenceSolutionRepository; + private final ObjectStorageService objectStorageService; + private final TestGenerationRequestProducer testGenerationRequestProducer; + + public AssignmentService(AssignmentRepository assignmentRepository, + TestCaseRepository testCaseRepository, + ReferenceSolutionRepository referenceSolutionRepository, + ObjectStorageService objectStorageService, + TestGenerationRequestProducer testGenerationRequestProducer) { + this.assignmentRepository = assignmentRepository; + this.testCaseRepository = testCaseRepository; + this.referenceSolutionRepository = referenceSolutionRepository; + this.objectStorageService = objectStorageService; + this.testGenerationRequestProducer = testGenerationRequestProducer; + } + + @Transactional + public AssignmentDTO createAssignment(CreateAssignmentRequest request, + MultipartFile referenceSolutionFile, + List testCaseInputFiles) { + // Persist the assignment as inactive until output generation completes + Assignment assignment = new Assignment( + request.getTitle(), + request.getDescription(), + request.getTimeLimitMs(), + request.getMemoryLimitMb(), + request.getComparatorType() + ); + assignment.setConstraints(request.getConstraints() != null ? request.getConstraints() : new ArrayList<>()); + assignment.setHints(request.getHints() != null ? request.getHints() : new ArrayList<>()); + assignment.setTags(request.getTags() != null ? request.getTags() : new ArrayList<>()); + assignment.setAllowedLanguages(request.getAllowedLanguages()); + assignment.setDueDate(request.getDueDate()); + assignment.setIsActive(false); + + assignment = assignmentRepository.save(assignment); + logger.info("Assignment created: id={}, title={}", assignment.getId(), assignment.getTitle()); + + // Persist reference solution metadata + ReferenceSolution referenceSolution = new ReferenceSolution(assignment, request.getReferenceLanguage()); + referenceSolution = referenceSolutionRepository.save(referenceSolution); + logger.info("ReferenceSolution created: id={}, language={}", referenceSolution.getId(), referenceSolution.getLanguage()); + + // Upload reference solution file to MinIO + String refExtension = FileExtensionUtil.getFileExtensionByLanguage(request.getReferenceLanguage()); + String refPath = ObjectKeyBuilder.referenceSolutionSourceCode(assignment.getId(), refExtension); + uploadMultipartFile(refPath, referenceSolutionFile); + logger.info("Reference solution uploaded: path={}", refPath); + + // Persist test cases and upload inputs; build job payload + List testCaseInfos = new ArrayList<>(); + for (int i = 0; i < testCaseInputFiles.size(); i++) { + boolean isSample = request.getSampleFlags() != null + && i < request.getSampleFlags().size() + && Boolean.TRUE.equals(request.getSampleFlags().get(i)); + + TestCase testCase = new TestCase(assignment, i + 1, isSample); + testCase = testCaseRepository.save(testCase); + logger.info("TestCase created: id={}, order={}, isSample={}", testCase.getId(), testCase.getOrder(), testCase.getIsSample()); + + String inputPath = ObjectKeyBuilder.testCaseInput(assignment.getId(), testCase.getId()); + String outputPath = ObjectKeyBuilder.testCaseOutput(assignment.getId(), testCase.getId()); + + uploadMultipartFile(inputPath, testCaseInputFiles.get(i)); + logger.info("Test case input uploaded: path={}", inputPath); + + testCaseInfos.add(new TestCaseInfo(testCase.getId(), inputPath, outputPath)); + } + + // Publish test generation job + TestGenerationJob job = new TestGenerationJob( + assignment.getId(), + refPath, + request.getReferenceLanguage(), + testCaseInfos, + request.getTimeLimitMs(), + request.getMemoryLimitMb() + ); + testGenerationRequestProducer.sendTestGenerationRequest(job); + logger.info("Test generation job published for assignmentId={}", assignment.getId()); + + return AssignmentMapper.toDTO(assignment); + } + + private void uploadMultipartFile(String path, MultipartFile file) { + try { + objectStorageService.upload(path, new String(file.getBytes())); + } catch (IOException e) { + throw new RuntimeException("Failed to read uploaded file: " + file.getOriginalFilename(), e); + } catch (Exception e) { + throw new RuntimeException("Failed to upload file to object storage: " + path, e); + } + } +} diff --git a/codehive-backend/src/main/java/com/github/codehive/service/ExecutionRequestService.java b/codehive-backend/src/main/java/com/github/codehive/service/ExecutionRequestService.java index 73a59e5..d020ec9 100644 --- a/codehive-backend/src/main/java/com/github/codehive/service/ExecutionRequestService.java +++ b/codehive-backend/src/main/java/com/github/codehive/service/ExecutionRequestService.java @@ -1,142 +1,148 @@ package com.github.codehive.service; +import java.util.List; import java.util.UUID; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.github.codehive.messaging.producer.ExecutionRequestProducer; import com.github.codehive.model.dto.ExecutionDTO; import com.github.codehive.model.dto.queue.ExecutionJob; +import com.github.codehive.model.entity.Assignment; import com.github.codehive.model.entity.Execution; +import com.github.codehive.model.entity.ReferenceSolution; import com.github.codehive.model.entity.User; -import com.github.codehive.model.enums.ComparatorType; import com.github.codehive.model.enums.ExecutionType; -import com.github.codehive.model.enums.Language; import com.github.codehive.model.exception.EntityNotFoundException; import com.github.codehive.model.mapper.ExecutionMapper; import com.github.codehive.model.request.execution.ExecutionRequest; +import com.github.codehive.repository.AssignmentRepository; import com.github.codehive.repository.ExecutionRepository; +import com.github.codehive.repository.ReferenceSolutionRepository; +import com.github.codehive.repository.TestCaseRepository; import com.github.codehive.repository.UserRepository; import com.github.codehive.utils.FileExtensionUtil; import com.github.codehive.utils.ObjectKeyBuilder; -/** - * - * TODO: Fix this class for a general execution request service. Currently, it is only for manual test execution. - * - */ - @Service public class ExecutionRequestService { - private static final Logger logger = LoggerFactory.getLogger(ExecutionRequestService.class); - - private static final Long DEFAULT_TIME_LIMIT_MS = 1000L; - private static final Long DEFAULT_MEMORY_LIMIT_MB = 256L; - + private final ExecutionRequestProducer executionRequestProducer; private final ExecutionRepository executionRepository; private final ObjectStorageService objectStorageService; private final UserRepository userRepository; - + private final AssignmentRepository assignmentRepository; + private final ReferenceSolutionRepository referenceSolutionRepository; + private final TestCaseRepository testCaseRepository; + public ExecutionRequestService(ExecutionRequestProducer executionRequestProducer, ExecutionRepository executionRepository, ObjectStorageService objectStorageService, - UserRepository userRepository) { + UserRepository userRepository, + AssignmentRepository assignmentRepository, + ReferenceSolutionRepository referenceSolutionRepository, + TestCaseRepository testCaseRepository) { this.executionRequestProducer = executionRequestProducer; this.executionRepository = executionRepository; this.objectStorageService = objectStorageService; this.userRepository = userRepository; + this.assignmentRepository = assignmentRepository; + this.referenceSolutionRepository = referenceSolutionRepository; + this.testCaseRepository = testCaseRepository; } @Transactional public ExecutionDTO requestExecution(ExecutionRequest request) { - logger.info("[WORKFLOW] Step 1: Creating execution entity - language={}, executionType={}, assignmentId={}", - request.getLanguage(), request.getExecutionType(), request.getAssignmentId()); - + Assignment assignment = assignmentRepository.findById(request.getAssignmentId()) + .orElseThrow(() -> new EntityNotFoundException( + "Assignment not found with id: " + request.getAssignmentId())); + Execution execution = new Execution(request.getExecutionType()); - + if (request.getRequesterId() != null) { User user = userRepository.findById(request.getRequesterId()) - .orElseThrow(() -> new EntityNotFoundException("User not found with id: " + request.getRequesterId())); + .orElseThrow(() -> new EntityNotFoundException( + "User not found with id: " + request.getRequesterId())); execution.setUser(user); } - + execution = executionRepository.save(execution); - logger.info("[WORKFLOW] Step 2: Execution entity saved to database - executionId={}, status={}", - execution.getId(), execution.getStatus()); - + String extension = FileExtensionUtil.getFileExtensionByLanguage(request.getLanguage()); String codeStorageKey = ObjectKeyBuilder.executionSourceCode(execution.getId(), extension); - - logger.info("[WORKFLOW] Step 3: Uploading source code to MinIO - key={}, codeLength={}", - codeStorageKey, request.getCode() != null ? request.getCode().length() : 0); - + try { objectStorageService.upload(codeStorageKey, request.getCode()); - logger.info("[WORKFLOW] Step 3: Source code uploaded successfully - key={}", codeStorageKey); } catch (Exception e) { - logger.error("[WORKFLOW] Step 3 FAILED: Could not upload source code - executionId={}, error={}", - execution.getId(), e.getMessage(), e); throw new RuntimeException("Failed to upload source code to object storage", e); } - ExecutionJob job = buildExecutionJob(execution, request, extension); - logger.info("[WORKFLOW] Step 4: Built ExecutionJob - executionId={}, sourceKey={}, outputKey={}, timeLimitMs={}, memoryLimitMb={}", - job.getId(), job.getSource(), job.getOutputPath(), job.getTimeLimitMs(), job.getMemoryLimitMb()); - - logger.info("[WORKFLOW] Step 5: Sending execution job to RabbitMQ queue"); + ExecutionJob job = buildExecutionJob(execution, request, assignment, extension); executionRequestProducer.sendExecutionRequest(job); - - logger.info("[WORKFLOW] Execution request workflow completed - executionId={}", execution.getId()); + return ExecutionMapper.toDTO(execution); } @Transactional(readOnly = true) public ExecutionDTO getExecutionById(UUID id) { Execution execution = executionRepository.findById(id) - .orElseThrow(() -> new EntityNotFoundException("Execution not found with id: " + id)); + .orElseThrow(() -> new EntityNotFoundException("Execution not found with id: " + id)); return ExecutionMapper.toDTO(execution); } - - private ExecutionJob buildExecutionJob(Execution execution, ExecutionRequest request, String extension) { + + private ExecutionJob buildExecutionJob(Execution execution, ExecutionRequest request, + Assignment assignment, String extension) { String sourceKey = ObjectKeyBuilder.executionSourceCode(execution.getId(), extension); String outputKey = ObjectKeyBuilder.executionTestCaseOutput(execution.getId()); - + if (request.getExecutionType() == ExecutionType.PRACTICE) { + ReferenceSolution referenceSolution = resolveReferenceSolution(assignment); + String refExtension = FileExtensionUtil.getFileExtensionByLanguage(referenceSolution.getLanguage()); + String refPath = ObjectKeyBuilder.referenceSolutionSourceCode(assignment.getId(), refExtension); + + List testCases = request.getTestCases(); return new ExecutionJob( - execution.getId(), - sourceKey, - ObjectKeyBuilder.referenceSolutionSourceCode(request.getAssignmentId(), "java"), // This will depend on assignment settings in future - request.getLanguage(), - request.getExecutionType(), - request.getTestCases(), - DEFAULT_TIME_LIMIT_MS, // This will depend on assignment settings in future - DEFAULT_MEMORY_LIMIT_MB, // This will depend on assignment settings in future - ComparatorType.EXACT_MATCH, // This will depend on assignment settings in future - outputKey, - request.getTestCases() != null ? request.getTestCases().size() : 0, - ObjectKeyBuilder.testsPath(request.getAssignmentId()), - Language.JAVA // This will deépend on assignment settings in future + execution.getId(), + sourceKey, + refPath, + request.getLanguage(), + request.getExecutionType(), + testCases, + assignment.getTimeLimitMs(), + assignment.getMemoryLimitMb(), + assignment.getComparatorType(), + outputKey, + testCases != null ? testCases.size() : 0, + ObjectKeyBuilder.testsPath(assignment.getId()), + referenceSolution.getLanguage() ); } else { + long numTests = testCaseRepository.countByAssignmentId(assignment.getId()); return new ExecutionJob( - execution.getId(), - sourceKey, - null, - request.getLanguage(), - request.getExecutionType(), - null, - DEFAULT_TIME_LIMIT_MS, // This will depend on assignment settings in future - DEFAULT_MEMORY_LIMIT_MB, // This will depend on assignment settings in future - ComparatorType.EXACT_MATCH, // This will depend on assignment settings in future - outputKey, - 5, // This will depend on assignment settings in future - ObjectKeyBuilder.testsPath(request.getAssignmentId()), - Language.JAVA // This will depend on assignment settings in future + execution.getId(), + sourceKey, + null, + request.getLanguage(), + request.getExecutionType(), + null, + assignment.getTimeLimitMs(), + assignment.getMemoryLimitMb(), + assignment.getComparatorType(), + outputKey, + (int) numTests, + ObjectKeyBuilder.testsPath(assignment.getId()), + null ); } } + + private ReferenceSolution resolveReferenceSolution(Assignment assignment) { + List solutions = referenceSolutionRepository.findByAssignmentId(assignment.getId()); + if (solutions.isEmpty()) { + throw new EntityNotFoundException( + "No reference solution found for assignment: " + assignment.getId()); + } + return solutions.get(0); + } } diff --git a/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java b/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java index e87fc7c..86a39e1 100644 --- a/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java +++ b/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java @@ -15,12 +15,12 @@ public static String testsPath(UUID assignmentId) { return String.format("test-suites/assignments/%s/", assignmentId); } - public static String submissionSourceCode(UUID assignmentId, UUID submissionId, String fileExtension, UUID groupId) { - return String.format("submissions/groups/%s/assignments/%s/submission-%s/Main.%s", groupId, assignmentId, submissionId, fileExtension); + public static String submissionSourceCode(UUID assignmentId, UUID submissionId, String fileExtension) { + return String.format("submissions/assignments/%s/submission-%s/Main.%s", assignmentId, submissionId, fileExtension); } - public static String executionOutput(UUID submissionId, UUID executionId, UUID groupId, String fileExtension) { - return String.format("executions/groups/%s/assignments/%s/execution-%s/output.%s", groupId, submissionId, executionId, fileExtension); + public static String executionOutput(UUID submissionId, UUID executionId, String fileExtension) { + return String.format("executions/assignments/%s/execution-%s/output.%s", submissionId, executionId, fileExtension); } public static String referenceSolutionSourceCode(UUID assignmentId, String fileExtension) { diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/config/RabbitMQConfig.java b/codehive-worker/src/main/java/com/github/codehive/worker/config/RabbitMQConfig.java index a7b6cd8..b6d5254 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/config/RabbitMQConfig.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/config/RabbitMQConfig.java @@ -10,6 +10,8 @@ public class RabbitMQConfig { public static final String QUEUE_NAME = System.getProperty("rabbitmq.queue", "codehive_queue"); public static final String RESULT_QUEUE_NAME = System.getProperty("rabbitmq.result.queue", "codehive_result_queue"); + public static final String TEST_GENERATION_QUEUE_NAME = System.getProperty("rabbitmq.test-generation.queue", "codehive_test_generation_queue"); + public static final String TEST_GENERATION_RESULT_QUEUE_NAME = System.getProperty("rabbitmq.test-generation.result.queue", "codehive_test_generation_result_queue"); @Bean Queue executionQueue() { @@ -21,6 +23,16 @@ Queue resultQueue() { return new Queue(RESULT_QUEUE_NAME, true); } + @Bean + Queue testGenerationQueue() { + return new Queue(TEST_GENERATION_QUEUE_NAME, true); + } + + @Bean + Queue testGenerationResultQueue() { + return new Queue(TEST_GENERATION_RESULT_QUEUE_NAME, true); + } + @Bean @SuppressWarnings("removal") public MessageConverter jsonMessageConverter() { diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/TestGenerationRequestListener.java b/codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/TestGenerationRequestListener.java new file mode 100644 index 0000000..1d38b9a --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/TestGenerationRequestListener.java @@ -0,0 +1,43 @@ +package com.github.codehive.worker.messaging.listener; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +import com.github.codehive.worker.messaging.producer.TestGenerationResultProducer; +import com.github.codehive.worker.model.dto.queue.TestGenerationJob; +import com.github.codehive.worker.model.dto.queue.TestGenerationResult; +import com.github.codehive.worker.service.TestGenerationService; + +@Component +public class TestGenerationRequestListener { + private static final Logger logger = LoggerFactory.getLogger(TestGenerationRequestListener.class); + + private final TestGenerationService testGenerationService; + private final TestGenerationResultProducer testGenerationResultProducer; + + public TestGenerationRequestListener(TestGenerationService testGenerationService, + TestGenerationResultProducer testGenerationResultProducer) { + this.testGenerationService = testGenerationService; + this.testGenerationResultProducer = testGenerationResultProducer; + } + + @RabbitListener(queues = "${rabbitmq.test-generation.queue:codehive_test_generation_queue}") + public void handleTestGenerationJob(TestGenerationJob job) { + logger.info("[WORKFLOW] RABBITMQ RECEIVE: Test generation job received - assignmentId={}, testCases={}, language={}", + job.getAssignmentId(), job.getTestCases().size(), job.getReferenceLanguage()); + + TestGenerationResult result; + try { + result = testGenerationService.generateOutputs(job); + } catch (Exception e) { + logger.error("[WORKFLOW] Unexpected error during test generation - assignmentId={}", + job.getAssignmentId(), e); + result = new TestGenerationResult(job.getAssignmentId(), false, 0, + "Unexpected error: " + e.getMessage()); + } + + testGenerationResultProducer.sendTestGenerationResult(result); + } +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/messaging/producer/TestGenerationResultProducer.java b/codehive-worker/src/main/java/com/github/codehive/worker/messaging/producer/TestGenerationResultProducer.java new file mode 100644 index 0000000..239b598 --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/messaging/producer/TestGenerationResultProducer.java @@ -0,0 +1,26 @@ +package com.github.codehive.worker.messaging.producer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.stereotype.Service; + +import com.github.codehive.worker.config.RabbitMQConfig; +import com.github.codehive.worker.model.dto.queue.TestGenerationResult; + +@Service +public class TestGenerationResultProducer { + private static final Logger logger = LoggerFactory.getLogger(TestGenerationResultProducer.class); + + private final RabbitTemplate rabbitTemplate; + + public TestGenerationResultProducer(RabbitTemplate rabbitTemplate) { + this.rabbitTemplate = rabbitTemplate; + } + + public void sendTestGenerationResult(TestGenerationResult result) { + logger.info("Sending test generation result - assignmentId={}, success={}, generated={}", + result.getAssignmentId(), result.isSuccess(), result.getGeneratedCount()); + rabbitTemplate.convertAndSend(RabbitMQConfig.TEST_GENERATION_RESULT_QUEUE_NAME, result); + } +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/queue/TestCaseInfo.java b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/queue/TestCaseInfo.java new file mode 100644 index 0000000..422edb9 --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/queue/TestCaseInfo.java @@ -0,0 +1,42 @@ +package com.github.codehive.worker.model.dto.queue; + +import java.util.UUID; + +public class TestCaseInfo { + private UUID testCaseId; + private String inputPath; + private String outputPath; + + public TestCaseInfo() { + } + + public TestCaseInfo(UUID testCaseId, String inputPath, String outputPath) { + this.testCaseId = testCaseId; + this.inputPath = inputPath; + this.outputPath = outputPath; + } + + public UUID getTestCaseId() { + return testCaseId; + } + + public void setTestCaseId(UUID testCaseId) { + this.testCaseId = testCaseId; + } + + public String getInputPath() { + return inputPath; + } + + public void setInputPath(String inputPath) { + this.inputPath = inputPath; + } + + public String getOutputPath() { + return outputPath; + } + + public void setOutputPath(String outputPath) { + this.outputPath = outputPath; + } +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/queue/TestGenerationJob.java b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/queue/TestGenerationJob.java new file mode 100644 index 0000000..121537f --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/queue/TestGenerationJob.java @@ -0,0 +1,66 @@ +package com.github.codehive.worker.model.dto.queue; + +import java.util.List; +import java.util.UUID; + +import com.github.codehive.worker.model.enums.Language; + +public class TestGenerationJob { + private UUID assignmentId; + private String referenceSolutionPath; + private Language referenceLanguage; + private List testCases; + private Long timeLimitMs; + private Long memoryLimitMb; + + public TestGenerationJob() { + } + + public UUID getAssignmentId() { + return assignmentId; + } + + public void setAssignmentId(UUID assignmentId) { + this.assignmentId = assignmentId; + } + + public String getReferenceSolutionPath() { + return referenceSolutionPath; + } + + public void setReferenceSolutionPath(String referenceSolutionPath) { + this.referenceSolutionPath = referenceSolutionPath; + } + + public Language getReferenceLanguage() { + return referenceLanguage; + } + + public void setReferenceLanguage(Language referenceLanguage) { + this.referenceLanguage = referenceLanguage; + } + + public List getTestCases() { + return testCases; + } + + public void setTestCases(List testCases) { + this.testCases = testCases; + } + + public Long getTimeLimitMs() { + return timeLimitMs; + } + + public void setTimeLimitMs(Long timeLimitMs) { + this.timeLimitMs = timeLimitMs; + } + + public Long getMemoryLimitMb() { + return memoryLimitMb; + } + + public void setMemoryLimitMb(Long memoryLimitMb) { + this.memoryLimitMb = memoryLimitMb; + } +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/queue/TestGenerationResult.java b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/queue/TestGenerationResult.java new file mode 100644 index 0000000..5484bd4 --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/queue/TestGenerationResult.java @@ -0,0 +1,52 @@ +package com.github.codehive.worker.model.dto.queue; + +import java.util.UUID; + +public class TestGenerationResult { + private UUID assignmentId; + private boolean success; + private int generatedCount; + private String errorMessage; + + public TestGenerationResult() { + } + + public TestGenerationResult(UUID assignmentId, boolean success, int generatedCount, String errorMessage) { + this.assignmentId = assignmentId; + this.success = success; + this.generatedCount = generatedCount; + this.errorMessage = errorMessage; + } + + public UUID getAssignmentId() { + return assignmentId; + } + + public void setAssignmentId(UUID assignmentId) { + this.assignmentId = assignmentId; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public int getGeneratedCount() { + return generatedCount; + } + + public void setGeneratedCount(int generatedCount) { + this.generatedCount = generatedCount; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } +} diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/service/TestGenerationService.java b/codehive-worker/src/main/java/com/github/codehive/worker/service/TestGenerationService.java new file mode 100644 index 0000000..324e62b --- /dev/null +++ b/codehive-worker/src/main/java/com/github/codehive/worker/service/TestGenerationService.java @@ -0,0 +1,96 @@ +package com.github.codehive.worker.service; + +import java.io.InputStream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import com.github.codehive.worker.model.dto.ExecutionResult; +import com.github.codehive.worker.model.dto.queue.TestCaseInfo; +import com.github.codehive.worker.model.dto.queue.TestGenerationJob; +import com.github.codehive.worker.model.dto.queue.TestGenerationResult; +import com.github.codehive.worker.model.enums.ExecutionStatus; +import com.github.codehive.worker.sandbox.LanguageExecutor; +import com.github.codehive.worker.sandbox.factory.LanguageExecutorFactory; + +@Service +public class TestGenerationService { + private static final Logger logger = LoggerFactory.getLogger(TestGenerationService.class); + + private final LanguageExecutorFactory executorFactory; + private final ObjectStorageService objectStorageService; + + public TestGenerationService(LanguageExecutorFactory executorFactory, + ObjectStorageService objectStorageService) { + this.executorFactory = executorFactory; + this.objectStorageService = objectStorageService; + } + + public TestGenerationResult generateOutputs(TestGenerationJob job) { + logger.info("[WORKFLOW] Starting test output generation - assignmentId={}, testCases={}", + job.getAssignmentId(), job.getTestCases().size()); + + LanguageExecutor executor; + try { + executor = executorFactory.getExecutor(job.getReferenceLanguage()); + } catch (Exception e) { + logger.error("[WORKFLOW] Unsupported reference language: {}", job.getReferenceLanguage(), e); + return new TestGenerationResult(job.getAssignmentId(), false, 0, + "Unsupported language: " + job.getReferenceLanguage()); + } + + // Verify the reference solution compiles before running all test cases + try { + InputStream refSource = objectStorageService.download(job.getReferenceSolutionPath()); + ExecutionResult compileCheck = executor.execute(refSource, null, + job.getTimeLimitMs(), job.getMemoryLimitMb()); + if (compileCheck.getStatus() == ExecutionStatus.CE) { + logger.error("[WORKFLOW] Reference solution compilation failed - assignmentId={}", + job.getAssignmentId()); + return new TestGenerationResult(job.getAssignmentId(), false, 0, + "Reference solution compilation error: " + compileCheck.getCompilationError()); + } + } catch (Exception e) { + logger.error("[WORKFLOW] Failed to compile-check reference solution - assignmentId={}", + job.getAssignmentId(), e); + return new TestGenerationResult(job.getAssignmentId(), false, 0, + "Failed to verify reference solution: " + e.getMessage()); + } + + int generated = 0; + for (TestCaseInfo tc : job.getTestCases()) { + try { + InputStream refSource = objectStorageService.download(job.getReferenceSolutionPath()); + InputStream inputStream = objectStorageService.download(tc.getInputPath()); + + ExecutionResult result = executor.execute(refSource, inputStream, + job.getTimeLimitMs(), job.getMemoryLimitMb()); + + if (result.getStatus() != ExecutionStatus.AC) { + logger.error("[WORKFLOW] Reference solution failed on testCaseId={}, status={}", + tc.getTestCaseId(), result.getStatus()); + return new TestGenerationResult(job.getAssignmentId(), false, generated, + "Reference solution failed on testCaseId=" + tc.getTestCaseId() + + " with status=" + result.getStatus()); + } + + String output = result.getOutput() != null ? result.getOutput() : ""; + objectStorageService.upload(tc.getOutputPath(), output); + generated++; + + logger.info("[WORKFLOW] Output generated for testCaseId={}, outputPath={}", + tc.getTestCaseId(), tc.getOutputPath()); + + } catch (Exception e) { + logger.error("[WORKFLOW] Error generating output for testCaseId={}", tc.getTestCaseId(), e); + return new TestGenerationResult(job.getAssignmentId(), false, generated, + "Error on testCaseId=" + tc.getTestCaseId() + ": " + e.getMessage()); + } + } + + logger.info("[WORKFLOW] Test output generation complete - assignmentId={}, generated={}", + job.getAssignmentId(), generated); + return new TestGenerationResult(job.getAssignmentId(), true, generated, null); + } +} diff --git a/llms/backend/OVERVIEW.md b/llms/backend/OVERVIEW.md index 109d8ee..094bc03 100644 --- a/llms/backend/OVERVIEW.md +++ b/llms/backend/OVERVIEW.md @@ -1,18 +1,19 @@ # Backend Overview ## What This Component Does -The backend is the central API for CodeHive. It handles authentication, user flows, execution orchestration, persistence, and integration with infrastructure services such as PostgreSQL, RabbitMQ, and object storage. +The backend is the central API for CodeHive. It handles authentication, user flows, assignment management, execution orchestration, persistence, and integration with PostgreSQL, RabbitMQ, and MinIO. Main responsibilities: - Expose REST endpoints used by the frontend. - Validate and authorize requests. - Execute business logic in service classes. - Persist and query data with JPA repositories. -- Publish and consume execution-related messages. +- Publish and consume execution-related and assignment-related messages. - Return standardized API responses and errors. ## How It Works -Typical request flow: + +### Typical request flow 1. A client calls a controller endpoint. 2. Request payload is validated. 3. Security and rate-limiting checks are applied. @@ -20,12 +21,19 @@ Typical request flow: 5. Service uses repositories, utilities, messaging, or external integrations. 6. A structured response is returned to the client. -Execution flow at a high level: -1. Backend receives an execution request. -2. Backend stores required metadata and/or files. -3. Backend publishes a message to RabbitMQ for the worker. -4. Worker processes the job and publishes results. -5. Backend consumes the result and exposes it through API endpoints. +### Assignment creation flow (teacher) +1. Teacher sends multipart request with metadata, reference solution file, and test case input files. +2. AssignmentService persists entities and uploads files to MinIO. +3. Backend publishes TestGenerationJob to `codehive_test_generation_queue`. +4. Worker generates expected outputs and publishes TestGenerationResult. +5. Backend activates the assignment on success. + +### Student execution flow +1. Backend receives an ExecutionRequest. +2. Loads Assignment to get real time/memory limits and comparator. +3. Stores source code in MinIO and publishes ExecutionJob to `codehive_queue`. +4. Worker processes the job and publishes ExecutionReport to `codehive_result_queue`. +5. Backend updates Execution status; client polls GET /api/execution/check/{id}. ## Useful Commands Run these from codehive-backend. @@ -39,7 +47,7 @@ Tests and coverage: - ./gradlew test - ./gradlew jacocoTestReport -Infrastructure services (from codehive-backend): +Infrastructure services: - docker compose up -d - docker compose down @@ -49,16 +57,16 @@ Common local endpoints: ## Project Folder Structure Runtime module structure (codehive-backend/src/main/java/com/github/codehive): -- config: Spring and infrastructure configuration. -- controller: HTTP entry points. +- config: Spring and infrastructure configuration (RabbitMQ, MinIO, Security, WebSocket, Async). +- controller: HTTP entry points (Auth, RecoveryPassword, CheckExecution, Assignment). - service: Business logic and orchestration. -- repository: Data access layer. +- repository: Data access layer (JpaRepository for all). - model: Entities, DTOs, requests, responses, exceptions. -- security: Auth and security-related classes. -- messaging: Queue listeners/producers and messaging contracts. -- ratelimit: Request throttling concerns. -- websocket: Realtime communication support. -- utils: Shared utility helpers. +- security: Auth filter and UserDetailsService. +- messaging: Queue listeners/producers for execution and test generation. +- ratelimit: Request throttling via @RateLimit annotation and aspect. +- websocket: CSV progress streaming. +- utils: ObjectKeyBuilder, FileExtensionUtil, JwtUtil, PasswordGenerator. Testing structure: - codehive-backend/src/test/java for unit and integration tests. @@ -67,7 +75,7 @@ Backend docs structure in llms/backend: - OVERVIEW.md: This document. - auth: Authentication and authorization details. - controller: Controller-level conventions and API patterns. -- executions: Execution request and result lifecycle. +- executions: Assignment creation, execution request and result lifecycle. - messaging: Queue contracts, listeners, and producers. - model: Data model and DTO conventions. - security: Security architecture and policies. diff --git a/llms/backend/controller/README.md b/llms/backend/controller/README.md index cc4e3ad..9960ba0 100644 --- a/llms/backend/controller/README.md +++ b/llms/backend/controller/README.md @@ -7,6 +7,7 @@ Controller classes: - codehive-backend/src/main/java/com/github/codehive/controller/AuthController.java - codehive-backend/src/main/java/com/github/codehive/controller/RecoveryPasswordController.java - codehive-backend/src/main/java/com/github/codehive/controller/CheckExecutionController.java +- codehive-backend/src/main/java/com/github/codehive/controller/AssignmentController.java ## Controller Responsibilities - Define API routes and HTTP semantics. @@ -17,35 +18,42 @@ Controller classes: - Delegate business logic to service classes. ## Response Pattern -Success responses typically use: +Success responses use: - SuccessResponse for data payloads. Error handling is centralized in: - model/exception/handler/GlobalExceptionHandler.java ## Endpoint Group Behavior -AuthController: + +### AuthController - Login, signup, current user, CSV bulk signup. -- Signup routes require admin authority. +- Signup routes require `@PreAuthorize("hasAuthority('ADMIN')")`. -RecoveryPasswordController: +### RecoveryPasswordController - Forgot password and reset password flows. - Returns generic success messages to avoid account enumeration. -CheckExecutionController: -- POST /api/execution/check creates execution and queues worker job. -- GET /api/execution/check/{id} polls execution status. +### CheckExecutionController +- `POST /api/execution/check` — creates execution and queues worker job. +- `GET /api/execution/check/{id}` — polls execution status. + +### AssignmentController +- `POST /api/assignments` — multipart; creates assignment, uploads files, queues test generation. +- Requires `@PreAuthorize("hasAnyRole('TEACHER', 'ADMIN')")`. +- Returns 202 Accepted immediately (test output generation is async). +- Request parts: `metadata` (JSON), `referenceSolution` (file), `testCaseInputs` (file list). ## Cross-Cutting Concerns -- OpenAPI annotations are used for API docs. -- Rate limiting is applied through custom annotation and aspect. +- OpenAPI annotations are used for API docs (Swagger at /swagger-ui.html). +- Rate limiting is applied through custom `@RateLimit` annotation and aspect. - Security is primarily in filters/config, not in controllers. ## Implementation Conventions - Keep controllers thin and orchestration-only. - Do not embed repository logic in controllers. -- Prefer explicit HTTP status codes (200, 201, 202, etc). -- Keep route grouping by domain under /api/{domain}. +- Prefer explicit HTTP status codes (200, 201, 202, 400, 403, 404). +- Keep route grouping by domain under `/api/{domain}`. ## Adding a New Controller 1. Create class under controller package. diff --git a/llms/backend/executions/README.md b/llms/backend/executions/README.md index af2d2fe..18e481d 100644 --- a/llms/backend/executions/README.md +++ b/llms/backend/executions/README.md @@ -1,75 +1,100 @@ # Backend Execution Orchestration ## Scope -This document explains how the backend receives execution requests, persists execution state, stores source code, and coordinates with the worker. +This document explains assignment creation, execution requests, and execution result processing. Primary classes: +- controller/AssignmentController.java - controller/CheckExecutionController.java +- service/AssignmentService.java - service/ExecutionRequestService.java - service/ExecutionResultService.java - messaging/producer/ExecutionRequestProducer.java +- messaging/producer/TestGenerationRequestProducer.java - messaging/listener/ExecutionResultListener.java +- messaging/listener/TestGenerationResultListener.java -## Request-to-Queue Flow -1. Client sends ExecutionRequest to POST /api/execution/check. -2. Controller validates payload and delegates to ExecutionRequestService. -3. Service creates Execution entity with status PENDING. -4. Source code is uploaded to MinIO via ObjectStorageService. -5. Service builds ExecutionJob payload. -6. Producer sends ExecutionJob to RabbitMQ queue. -7. API returns 202 Accepted with ExecutionDTO (for polling). +## Assignment Creation Flow (Teacher) +1. Teacher sends `POST /api/assignments` (multipart/form-data). + - Part `metadata`: JSON matching CreateAssignmentRequest. + - Part `referenceSolution`: source file. + - Part `testCaseInputs`: list of test case input files. +2. AssignmentService: + - Creates Assignment entity with `isActive = false`. + - Creates ReferenceSolution entity. + - Creates TestCase entities (order = upload position, isSample from sampleFlags). + - Uploads reference solution to `test-suites/assignments/{id}/reference/Main.{ext}`. + - Uploads each test case input to `test-suites/assignments/{id}/tc-{tcId}/tc{tcId}.in`. + - Publishes TestGenerationJob to `codehive_test_generation_queue`. +3. Worker generates expected outputs and publishes TestGenerationResult. +4. TestGenerationResultListener sets `assignment.isActive = true` on success. -## Result Processing Flow +The API returns 202 Accepted immediately — output generation is asynchronous. + +## Request-to-Queue Flow (Student Execution) +1. Client sends ExecutionRequest to `POST /api/execution/check`. +2. Controller validates and delegates to ExecutionRequestService. +3. Service loads Assignment to get real limits (timeLimitMs, memoryLimitMb, comparatorType). +4. Service creates Execution entity with status PENDING. +5. Source code is uploaded to MinIO via ObjectStorageService. +6. Service builds ExecutionJob with assignment-driven values. +7. Producer sends ExecutionJob to `codehive_queue`. +8. API returns 202 Accepted with ExecutionDTO (for polling). + +## Result Processing Flow (Student Execution) 1. Worker publishes ExecutionReport. 2. ExecutionResultListener consumes the report. 3. ExecutionResultService loads execution by id. -4. Status/time/memory are updated in the executions table. -5. Client retrieves updated status via GET /api/execution/check/{id}. +4. Status, timeMs, and memoryMb are updated in the executions table. +5. Client retrieves updated status via `GET /api/execution/check/{id}`. ## Key Data Contracts -API request: +Assignment creation request: +- model/request/assignment/CreateAssignmentRequest.java + +Execution API request: - model/request/execution/ExecutionRequest.java -Queue request: +Queue payloads: - model/dto/queue/ExecutionJob.java - -Queue result: - model/dto/queue/ExecutionReport.java +- model/dto/queue/TestGenerationJob.java (contains list of TestCaseInfo) +- model/dto/queue/TestGenerationResult.java API polling response: - model/dto/ExecutionDTO.java -## Current Constraints and Defaults -- Time and memory limits are currently static defaults in ExecutionRequestService: - - time: 1000 ms - - memory: 256 MB -- Comparator type is currently fixed to EXACT_MATCH. -- Reference language is currently hard-coded in job building path. -- TODO comments indicate this is temporary and should become assignment-driven. +## Execution Job Construction +PRACTICE mode: +- Loads first ReferenceSolution for the assignment to get language and reference path. +- Uses inline testCases from the request. +- timeLimitMs, memoryLimitMb, comparatorType come from Assignment entity. -## Storage Keys and Artifacts -Object paths are generated through ObjectKeyBuilder: -- execution source keys -- execution output keys -- test suite and reference keys +DEFINITIVE mode: +- No reference solution path needed (outputs already in MinIO). +- numTests counted from TestCaseRepository. +- testsPath from ObjectKeyBuilder.testsPath(assignmentId). -File extension mapping is resolved by FileExtensionUtil based on Language enum. +## Storage Keys and Artifacts +Object paths are generated through ObjectKeyBuilder (utils/ObjectKeyBuilder.java). +File extension mapping resolved by FileExtensionUtil based on Language enum. ## Persistence Model -Execution entity fields track: -- executionType -- status -- timeMs -- memoryMb -- createdAt -- isOutdated -- optional user -- optional submission - -Repository: -- repository/ExecutionRepository.java +Assignment entity fields: +- title, description, constraints, hints, tags +- allowedLanguages (element collection) +- timeLimitMs, memoryLimitMb, comparatorType +- dueDate, createdAt, updatedAt +- isActive (false while generation in progress, true when ready) -## Extension Guidance -- Move defaults to assignment configuration and database-backed policies. -- Version queue payloads when adding fields to avoid producer/consumer drift. -- Keep execution polling backward compatible for frontend stability. +Execution entity fields: +- executionType, status (PENDING → final) +- timeMs, memoryMb (set from worker result) +- isOutdated, createdAt +- optional user, optional submission + +Repositories: +- repository/AssignmentRepository.java +- repository/ExecutionRepository.java +- repository/TestCaseRepository.java +- repository/ReferenceSolutionRepository.java diff --git a/llms/backend/messaging/README.md b/llms/backend/messaging/README.md index ff3eefa..011d234 100644 --- a/llms/backend/messaging/README.md +++ b/llms/backend/messaging/README.md @@ -1,51 +1,74 @@ # Backend Messaging Implementation ## Scope -This document covers backend message production/consumption for code execution workflows. +This document covers backend message production/consumption for code execution and assignment creation workflows. Key classes: - config/RabbitConfig.java - messaging/producer/ExecutionRequestProducer.java +- messaging/producer/TestGenerationRequestProducer.java - messaging/listener/ExecutionResultListener.java +- messaging/listener/TestGenerationResultListener.java ## Queue Topology -Configured in RabbitConfig: -- Request queue: codehive_queue (default, configurable by system property) -- Result queue: codehive_result_queue (default, configurable by system property) +All queues are declared durable with Jackson JSON message conversion. -Message converter: -- Jackson2JsonMessageConverter for JSON serialization/deserialization. +Configured in RabbitConfig via system properties (defaults shown): -## Producer Flow +| Constant | Default name | Direction | +|---|---|---| +| `QUEUE_NAME` | `codehive_queue` | backend → worker | +| `RESULT_QUEUE_NAME` | `codehive_result_queue` | worker → backend | +| `TEST_GENERATION_QUEUE_NAME` | `codehive_test_generation_queue` | backend → worker | +| `TEST_GENERATION_RESULT_QUEUE_NAME` | `codehive_test_generation_result_queue` | worker → backend | + +## Execution Flow (student submissions) + +### Producer ExecutionRequestProducer: -1. Receives ExecutionJob from service layer. -2. Logs execution context and limits. -3. Publishes to request queue using RabbitTemplate.convertAndSend. +1. Receives ExecutionJob from ExecutionRequestService. +2. Publishes to `codehive_queue` via RabbitTemplate.convertAndSend. +3. Rethrows on publish failure. -Produced payload model: -- model/dto/queue/ExecutionJob.java +Payload: `model/dto/queue/ExecutionJob` -## Consumer Flow +### Consumer ExecutionResultListener: -1. Listens on configured result queue. -2. Receives ExecutionReport payload. -3. Delegates to ExecutionResultService for database update. -4. Logs processing success/failures. +1. Listens on `codehive_result_queue`. +2. Receives ExecutionReport from worker. +3. Delegates to ExecutionResultService for execution entity update. + +Payload: `model/dto/queue/ExecutionReport` + +## Test Generation Flow (assignment creation) + +### Producer +TestGenerationRequestProducer: +1. Receives TestGenerationJob from AssignmentService after files are uploaded to MinIO. +2. Publishes to `codehive_test_generation_queue`. + +Payload: `model/dto/queue/TestGenerationJob` + +### Consumer +TestGenerationResultListener: +1. Listens on `codehive_test_generation_result_queue`. +2. On success: sets `assignment.isActive = true` in the database. +3. On failure: logs error — assignment stays inactive. -Consumed payload model: -- model/dto/queue/ExecutionReport.java +Payload: `model/dto/queue/TestGenerationResult` ## Reliability Notes -- Queues are declared durable in RabbitConfig. -- Listener catches processing errors and logs them to prevent silent failures. -- Producer rethrows publish exceptions. +- All queues are declared durable — messages survive broker restarts. +- Listeners catch and log processing errors to prevent silent failures. +- Producers rethrow publish exceptions so the caller can handle them. ## Compatibility Rules - Treat queue DTO changes as contract changes. - If fields are added, coordinate worker and backend deployment. - Keep default queue names stable unless infrastructure update is coordinated. +- Both sides (backend and worker) declare the same queues — no exchange routing is used. ## Operational Tips - Verify backend and worker use matching queue names. -- Keep RabbitMQ running before submitting execution requests. -- Use execution logs to trace request id and status transitions. +- Keep RabbitMQ running before starting either service. +- Use `[WORKFLOW]`-prefixed log lines to trace message flow end to end. diff --git a/llms/backend/model/README.md b/llms/backend/model/README.md index 7c52308..2dd509b 100644 --- a/llms/backend/model/README.md +++ b/llms/backend/model/README.md @@ -8,76 +8,62 @@ Package root: ## Entity Model Entities are plain JPA classes with explicit constraints via @Column and relationship annotations. +All primary keys use `java.util.UUID` with `@GeneratedValue(strategy = GenerationType.UUID)`. ### User File: model/entity/User.java - -Important constraints: - name: nullable=false, length=50 - lastName: nullable=false, length=80 - enrollmentNumber: nullable=false, unique=true, length=50 - email: nullable=false, unique=true, length=100 - password: nullable=false, length=255 - role: enum stored as string, length=15 - -Behavior notes: - Implements UserDetails for Spring Security integration. -- createdAt defaults to LocalDateTime.now(). -- isActive defaults to true. -- temporaryPassword defaults to false. +- createdAt defaults to LocalDateTime.now(); isActive defaults to true; temporaryPassword defaults to false. - scopes stored as element collection in user_scopes table. ### PasswordResetToken File: model/entity/PasswordResetToken.java - -Important constraints: -- token unique and non-null. -- expiryDate non-null. -- used non-null default false. -- ManyToOne relation to User (nullable=false). +- token: unique, non-null +- expiryDate: non-null; tokens expire after 15 minutes +- used: non-null, default false +- ManyToOne relation to User (nullable=false) ### Assignment File: model/entity/Assignment.java - -Important constraints: -- title length 200, non-null. -- description TEXT, non-null. -- timeLimitMs and memoryLimitMb non-null. -- comparatorType enum string, non-null. -- createdAt and updatedAt non-null. - -Collection-backed tables: -- assignment_constraints -- assignment_hints -- assignment_tags -- assignment_allowed_languages +- title: length 200, non-null +- description: TEXT, non-null +- timeLimitMs, memoryLimitMb: non-null +- comparatorType: enum string, non-null +- isActive: defaults to false on creation; set to true after worker confirms test output generation +- allowedLanguages, constraints, hints, tags: element collections in dedicated tables +- dueDate: nullable ### Submission File: model/entity/Submission.java -- ManyToOne assignment relation (non-null). -- language enum string (non-null). -- createdAt initialized in constructor. +- ManyToOne assignment (non-null) +- language: enum string (non-null) +- createdAt initialized in constructor ### Execution File: model/entity/Execution.java - -Important constraints and semantics: -- executionType and status are required enums. -- status defaults to PENDING. -- isOutdated defaults to false. -- submission is nullable (practice executions may have no submission). -- user relation is nullable (depends on requester context). +- executionType and status: required enums (status defaults to PENDING) +- isOutdated defaults to false +- submission nullable (practice executions have no submission) +- user nullable +- timeMs, memoryMb: set from worker result ### TestCase File: model/entity/TestCase.java -- assignment relation required. -- order stored as order_index, non-null. -- isSample default false. +- assignment relation required +- order stored as order_index, non-null; represents 1-based upload position +- isSample defaults to false ### ReferenceSolution File: model/entity/ReferenceSolution.java -- assignment relation required. -- language enum required. +- assignment relation required +- language: enum required +- one ReferenceSolution per assignment per language; stored in MinIO at ObjectKeyBuilder.referenceSolutionSourceCode ## Request Contract Structure Request classes live under model/request grouped by domain: @@ -86,27 +72,29 @@ Request classes live under model/request grouped by domain: - recovery/ForgotPasswordRequest - recovery/RecoveryPasswordRequest - execution/ExecutionRequest +- assignment/CreateAssignmentRequest -Validation patterns: -- @NotBlank for required strings. -- @NotNull for required enum fields. -- @Size for password and text boundaries. -- @Email for email format in signup. +### CreateAssignmentRequest fields +- title, description, constraints, hints, tags +- timeLimitMs (@Min 100), memoryLimitMb (@Min 16) +- comparatorType, allowedLanguages, referenceLanguage +- dueDate (nullable) +- sampleFlags: parallel list to uploaded files; true = sample test case -Examples of enforced constraints: -- login password min length 6. -- signup name 2..50. -- signup father/mother last name 2..40. -- recovery new password min length 6. +Validation patterns: +- @NotBlank for required strings +- @NotNull for required enum fields +- @NotEmpty for required collections +- @Min for numeric limits ## Response Contract Structure Base response pattern: - ApiResponse(success, message) Concrete wrappers: -- SuccessResponse for successful payload responses. -- ErrorResponse for timestamped API failures. -- MessageResponse for simple text payloads. +- SuccessResponse for successful payload responses +- ErrorResponse for timestamped API failures +- MessageResponse for simple text payloads Auth-specific responses: - auth/AuthResponse (token + UserDTO) @@ -114,30 +102,26 @@ Auth-specific responses: - auth/CsvProgressMessage ## DTO and Mapper Layer -DTOs mirror API-safe views and queue contracts. - -Application DTO examples: -- UserDTO -- ExecutionDTO -- AssignmentDTO -- SubmissionDTO +Application DTOs: +- UserDTO, ExecutionDTO, AssignmentDTO, SubmissionDTO, TestCaseDTO, ReferenceSolutionDTO -Queue DTO examples: -- queue/ExecutionJob -- queue/ExecutionReport +Queue DTOs (model/dto/queue): +- ExecutionJob — student execution job sent to worker +- ExecutionReport — student execution result from worker +- TestGenerationJob — assignment output generation job sent to worker; contains List +- TestCaseInfo — per-test-case input/output MinIO paths +- TestGenerationResult — outcome of output generation; drives assignment activation -Mappers convert entity <-> DTO, for example: -- ExecutionMapper -- UserMapper -- AssignmentMapper +Mappers convert entity <-> DTO: +- ExecutionMapper, UserMapper, AssignmentMapper, SubmissionMapper, TestCaseMapper, ReferenceSolutionMapper ## Enum Strategy Enums are persisted and transferred as string values: - Role, Scope -- Language -- ExecutionType -- ExecutionStatus -- ComparatorType +- Language (JAVA, PYTHON, C, CPP) +- ExecutionType (PRACTICE, DEFINITIVE) +- ExecutionStatus (AC, WA, CE, RTE, TLE, MLE, PENDING) +- ComparatorType (EXACT_MATCH, FLOATING_POINT) ## Exception Model Exception packages are domain-grouped: @@ -151,4 +135,5 @@ GlobalExceptionHandler maps exceptions to stable HTTP responses and ErrorRespons - Keep entity constraints aligned with request validation, not looser. - Prefer DTO exposure over returning entities directly. - If adding enum values, validate impact on frontend and worker contracts. -- Queue DTO evolution must be coordinated across services. +- Queue DTO evolution must be coordinated across backend and worker — both projects declare mirrored copies. +- All IDs are UUID, never Long. diff --git a/llms/backend/service/README.md b/llms/backend/service/README.md index e0f0f9f..0b52062 100644 --- a/llms/backend/service/README.md +++ b/llms/backend/service/README.md @@ -7,6 +7,7 @@ Service classes: - AuthService - RecoveryPasswordService - CsvRegistrationService +- AssignmentService - ExecutionRequestService - ExecutionResultService - ObjectStorageService @@ -26,10 +27,7 @@ Responsibilities: - JWT issuance and user retrieval by token. Key dependencies: -- UserRepository -- PasswordEncoder -- JwtUtil -- MailSenderService +- UserRepository, PasswordEncoder, JwtUtil, MailSenderService ## RecoveryPasswordService Responsibilities: @@ -39,10 +37,7 @@ Responsibilities: - Validate token (exists, not expired, not used) and update password. Key dependencies: -- PasswordResetTokenRepository -- UserRepository -- PasswordEncoder -- MailSenderService +- PasswordResetTokenRepository, UserRepository, PasswordEncoder, MailSenderService ## CsvRegistrationService Responsibilities: @@ -54,31 +49,50 @@ Execution model: - Runs with @Async. - Uses taskId routing to send progress to subscribed clients. +## AssignmentService +Responsibilities: +- Create Assignment, ReferenceSolution, and TestCase entities in one transaction. +- Upload reference solution and test case inputs to MinIO via ObjectStorageService. +- Publish TestGenerationJob to `codehive_test_generation_queue`. +- Assignment is created with `isActive = false`; activated only after worker confirms output generation. + +Key dependencies: +- AssignmentRepository, TestCaseRepository, ReferenceSolutionRepository +- ObjectStorageService, TestGenerationRequestProducer + +Key detail: `sampleFlags` from the request is a parallel list to the uploaded files indicating which test cases are samples. Missing flags default to false. + ## ExecutionRequestService Responsibilities: -- Create execution records. -- Persist source code to object storage. -- Build ExecutionJob payload. -- Send execution request to RabbitMQ. +- Load Assignment entity to get real time/memory limits, comparator, and test count. +- Load ReferenceSolution to determine reference language and storage path. +- Create Execution entity. +- Upload source code to MinIO. +- Build and publish ExecutionJob to `codehive_queue`. -Current implementation notes: -- Contains TODOs indicating current behavior is oriented to manual test execution. -- Uses default time/memory/comparator values pending assignment-driven policies. +Key dependencies: +- ExecutionRepository, AssignmentRepository, ReferenceSolutionRepository, TestCaseRepository +- ObjectStorageService, ExecutionRequestProducer, UserRepository + +PRACTICE mode: uses inline testCases, resolves reference solution from DB. +DEFINITIVE mode: numTests counted from TestCaseRepository, no reference solution needed at runtime. ## ExecutionResultService Responsibilities: - Consume worker reports (via listener call chain). -- Update execution status and resource metrics. +- Update execution status, timeMs, and memoryMb. - Persist result summary for polling clients. ## ObjectStorageService Responsibilities: -- Upload and download artifacts from MinIO bucket. -- Support both stream uploads and plain-text content uploads. +- Upload artifacts to MinIO bucket: stream or plain-text overloads. +- Download artifacts by object key. + +All keys follow ObjectKeyBuilder conventions. ## MailSenderService Responsibilities: -- Send password recovery emails. +- Send password recovery emails with reset links. - Send welcome emails with temporary credentials. Configuration-driven fields: diff --git a/llms/worker/OVERVIEW.md b/llms/worker/OVERVIEW.md index 2e6a898..15ba49c 100644 --- a/llms/worker/OVERVIEW.md +++ b/llms/worker/OVERVIEW.md @@ -1,29 +1,38 @@ # Worker Overview ## What This Component Does -The worker is a background Spring Boot service responsible for sandboxed code execution. It consumes execution jobs from RabbitMQ, runs code in isolated environments, evaluates outputs, and publishes results back to the system. +The worker is a background Spring Boot service responsible for sandboxed code execution and test output generation. It consumes jobs from RabbitMQ, runs code in isolated Docker environments, evaluates outputs, and publishes results back to the backend. Main responsibilities: -- Consume execution jobs from message queues. +- Consume student execution jobs and produce execution reports. +- Consume teacher assignment creation jobs and generate expected test outputs. - Select the proper language executor. - Run untrusted code with sandbox and timeout constraints. -- Compare produced output with expected output when required. -- Publish execution results to backend-facing queues. +- Compare produced output with expected output. +- Publish results to backend-facing queues. ## How It Works -Execution pipeline: -1. Backend publishes an execution job message. -2. Worker listener receives and validates the job. -3. Worker fetches required assets and input data. -4. Worker selects a language executor through a factory. -5. Code is compiled/run inside constrained execution environments. -6. Comparator logic validates output when test-based execution is used. -7. Worker publishes the final execution result message. + +### Student Execution Pipeline +1. Backend publishes ExecutionJob to `codehive_queue`. +2. ExecutionRequestListener receives and logs the job. +3. TestExecutionService runs the submission in a language executor. +4. Per-test results are aggregated and artifacts uploaded to MinIO. +5. ExecutionResultProducer publishes ExecutionReport to `codehive_result_queue`. + +### Test Generation Pipeline +1. Backend publishes TestGenerationJob to `codehive_test_generation_queue`. +2. TestGenerationRequestListener receives and delegates to TestGenerationService. +3. Service compile-checks reference solution, then executes it for each test case input. +4. Outputs are uploaded to MinIO at the expected output paths. +5. TestGenerationResultProducer publishes TestGenerationResult to `codehive_test_generation_result_queue`. +6. Backend activates the assignment on success. Safety and reliability concerns: - Timeout protection prevents runaway executions. - Language-specific executors isolate compile/run logic. -- Messaging decouples API traffic from execution workload. +- Compilation gate aborts both pipelines early on CE. +- Results (including failures) are always published — no silent job loss. ## Useful Commands Run these from codehive-worker. @@ -36,31 +45,33 @@ Testing: - ./gradlew test Operational dependency: -- RabbitMQ must be running before bootRun. +- RabbitMQ and MinIO must be running before bootRun. Typical local setup: -1. Start infrastructure from codehive-backend using docker compose up -d. +1. Start infrastructure from codehive-backend using `docker compose up -d`. 2. Run backend. 3. Run worker. ## Project Folder Structure Runtime module structure (codehive-worker/src/main/java/com/github/codehive/worker): -- config: Worker infrastructure configuration. -- messaging: Queue listeners and producers. -- model: Worker DTOs and enums. +- config: Worker infrastructure configuration (Docker client, MinIO, RabbitMQ). +- messaging/listener: ExecutionRequestListener, TestGenerationRequestListener. +- messaging/producer: ExecutionResultProducer, TestGenerationResultProducer. +- model/dto: ExecutionReport, TestCaseResult, ExecutionResult. +- model/dto/queue: ExecutionJob, TestGenerationJob, TestCaseInfo, TestGenerationResult. +- model/enums: Language, ExecutionType, ExecutionStatus, ComparatorType. - sandbox: Execution core and language implementations. -- service: Worker service-level integrations. -- WorkerApplication.java: Spring Boot entry point. +- service: TestExecutionService, TestGenerationService, ObjectStorageService, OutputComparatorService. Sandbox structure highlights: -- sandbox/factory: Executor selection. -- sandbox/java, sandbox/python, sandbox/c, sandbox/cpp: Language-specific execution strategies. +- sandbox/factory: Executor selection by Language enum. +- sandbox/java, sandbox/python, sandbox/c, sandbox/cpp: Language-specific strategies. - sandbox/LanguageExecutor.java: Executor contract. Worker docs structure in llms/worker: - OVERVIEW.md: This document. - comparator: Output comparison behavior and rules. -- execution: Execution lifecycle and orchestration. +- execution: Execution and generation lifecycle. - messaging: Queue payloads and message flow. - sandbox: Isolation, execution strategy, and constraints. diff --git a/llms/worker/execution/README.md b/llms/worker/execution/README.md index 2677628..a391fd3 100644 --- a/llms/worker/execution/README.md +++ b/llms/worker/execution/README.md @@ -5,70 +5,78 @@ This document describes how the worker executes incoming jobs, computes verdicts Primary classes: - codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/ExecutionRequestListener.java +- codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/TestGenerationRequestListener.java - codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java +- codehive-worker/src/main/java/com/github/codehive/worker/service/TestGenerationService.java - codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionReport.java - codehive-worker/src/main/java/com/github/codehive/worker/model/dto/TestCaseResult.java -## High-Level Flow +## Student Execution Flow (TestExecutionService) 1. ExecutionRequestListener receives ExecutionJob from RabbitMQ. -2. Listener delegates job to TestExecutionService.executeJob. -3. Service compiles/runs submission through the selected language executor. +2. Listener delegates to TestExecutionService.executeJob. +3. Service compile-checks the submission first; returns CE report immediately if it fails. 4. Per-test results are aggregated in ExecutionReport. -5. Report JSON and test artifacts are uploaded to object storage. +5. Test artifacts (stdout.txt, stderr.txt) and report.json are uploaded to MinIO. 6. Listener publishes final ExecutionReport to result queue. -## Execution Modes +### Execution Modes + DEFINITIVE: -- Uses testsPath and numTests from ExecutionJob. +- Uses `testsPath` and `numTests` from ExecutionJob. - For each test case: - - download input (.in) - - download expected output (.out) - - execute submission - - compare outputs + - download input (`{testsPath}tc-{n}/tc-{n}.in`) + - download expected output (`{testsPath}tc-{n}/tc-{n}.out`) + - execute submission with input + - compare outputs via OutputComparatorService PRACTICE: -- Uses inline testCases from ExecutionJob. -- Runs reference solution first to generate expected output. -- Runs submission and compares against reference output. +- Uses inline `testCases` list from ExecutionJob. +- Runs reference solution on each input to produce expected output. +- Runs submission and compares. -## Compilation Gate -- Service performs an initial compile/syntax validation pass before test loop. -- If compile fails: - - report.compilationError is set - - overall status becomes CE - - per-test execution is skipped +## Test Generation Flow (TestGenerationService) +Triggered by TestGenerationJob after a teacher creates an assignment. -## Test Artifact Output -For each test case, service uploads: -- stdout.txt -- stderr.txt (if non-empty) +1. Resolve the language executor for `referenceLanguage`. +2. Compile-check the reference solution (fast-fail before processing all test cases). +3. For each TestCaseInfo in the job: + - Download reference solution from `referenceSolutionPath`. + - Download test input from `inputPath`. + - Execute reference solution with the input. + - If result status is not AC, abort with failure result. + - Upload actual output to `outputPath` (MinIO). +4. Return TestGenerationResult: + - `success = true`, `generatedCount = N` if all outputs were produced. + - `success = false` with error message and partial count on any failure. -Report upload: -- report.json generated from ExecutionReport and uploaded to outputPath. +## Compilation Gate +Both services perform a compile check before the main execution loop. +- If compile fails (CE status), per-test execution is skipped entirely. +- TestGenerationService additionally rejects if reference solution returns non-AC. ## Verdict Model -Per-test verdicts are represented by ExecutionStatus values: +Per-test verdicts represented by ExecutionStatus: - AC, WA, CE, RTE, TLE, MLE, PENDING -Overall verdict in ExecutionReport is derived by priority: +Overall verdict in ExecutionReport derived by priority: 1. CE if compilationError exists 2. AC if all tests pass -3. otherwise TLE > MLE > RTE > WA +3. Otherwise: TLE > MLE > RTE > WA ## Runtime Constraints -Each run uses limits from ExecutionJob: +Each run uses limits from the job payload: - timeLimitMs - memoryLimitMb -Executors enforce limits in Docker using container constraints and timeout guards. +Executors enforce limits via Docker container constraints and timeout guards. ## Failure Handling -- Unhandled processing failures create an error report with RTE and compilationError summary. -- Listener still publishes error result to backend queue to avoid silent job loss. +- Unhandled failures produce an error report/result that is still published to backend queue. +- Silent job loss is avoided by always sending a result, even on unexpected errors. ## Extension Guidance -- Keep ExecutionJob and ExecutionReport contract-compatible with backend. -- If adding new statuses, update: - - ExecutionStatus enum - - overall status calculation - - backend result processing logic +- Keep ExecutionJob, ExecutionReport, TestGenerationJob, and TestGenerationResult contract-compatible with backend. +- If adding new ExecutionStatus values, update: + - ExecutionStatus enum in both backend and worker + - overall status calculation in ExecutionReport.determineOverallStatus + - backend result processing logic in ExecutionResultService diff --git a/llms/worker/messaging/README.md b/llms/worker/messaging/README.md index b7cbd12..7f77c70 100644 --- a/llms/worker/messaging/README.md +++ b/llms/worker/messaging/README.md @@ -1,62 +1,63 @@ # Worker Messaging Implementation ## Scope -This document describes how the worker consumes execution jobs and publishes execution results through RabbitMQ. +This document describes how the worker consumes jobs and publishes results through RabbitMQ. Key classes: - codehive-worker/src/main/java/com/github/codehive/worker/config/RabbitMQConfig.java - codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/ExecutionRequestListener.java +- codehive-worker/src/main/java/com/github/codehive/worker/messaging/listener/TestGenerationRequestListener.java - codehive-worker/src/main/java/com/github/codehive/worker/messaging/producer/ExecutionResultProducer.java +- codehive-worker/src/main/java/com/github/codehive/worker/messaging/producer/TestGenerationResultProducer.java ## Queue Topology -Configured queue names: -- request queue: codehive_queue (default) -- result queue: codehive_result_queue (default) - -Both queues are durable and use Jackson JSON message conversion. - -## Incoming Message Contract -Listener consumes: -- model/dto/queue/ExecutionJob - -Important fields in ExecutionJob: -- id -- source -- reference -- language -- referenceLanguage -- executionType -- comparatorType -- timeLimitMs -- memoryLimitMb -- outputPath -- testsPath, numTests, testCases - -## Consumption Flow -1. @RabbitListener receives ExecutionJob. -2. Listener logs full job context. -3. Listener invokes TestExecutionService.executeJob. -4. Successful run produces ExecutionReport. -5. Listener sends report through ExecutionResultProducer. - -Error path: -- If execution fails unexpectedly, listener builds fallback report: - - overallStatus = RTE - - compilationError = internal error message -- Fallback report is still published to backend. - -## Outgoing Message Contract -Producer publishes: -- model/dto/ExecutionReport - -Report includes: -- executionId -- overallStatus -- per-test results -- timing and memory summaries -- compilationError when present +All queues are durable. Jackson JSON message conversion is applied globally. + +| Constant | Default name | Worker role | +|---|---|---| +| `QUEUE_NAME` | `codehive_queue` | consumer | +| `RESULT_QUEUE_NAME` | `codehive_result_queue` | producer | +| `TEST_GENERATION_QUEUE_NAME` | `codehive_test_generation_queue` | consumer | +| `TEST_GENERATION_RESULT_QUEUE_NAME` | `codehive_test_generation_result_queue` | producer | + +## Student Execution Flow + +### Consumer — ExecutionRequestListener +1. @RabbitListener receives ExecutionJob from `codehive_queue`. +2. Listener logs full job context (id, language, type, paths, limits). +3. Delegates to TestExecutionService.executeJob. +4. Publishes result through ExecutionResultProducer. + +Error path: if execution throws unexpectedly, listener builds a fallback ExecutionReport with `overallStatus = RTE` and still publishes it. + +Incoming payload: `model/dto/queue/ExecutionJob` + +### Producer — ExecutionResultProducer +Publishes ExecutionReport to `codehive_result_queue`. + +Outgoing payload: `model/dto/ExecutionReport` + +## Test Generation Flow + +### Consumer — TestGenerationRequestListener +1. @RabbitListener receives TestGenerationJob from `codehive_test_generation_queue`. +2. Delegates to TestGenerationService.generateOutputs. +3. Publishes TestGenerationResult through TestGenerationResultProducer. + +Error path: uncaught exceptions produce a failed TestGenerationResult (success=false) which is still published. + +Incoming payload: `model/dto/queue/TestGenerationJob` +- Contains `referenceSolutionPath`, `referenceLanguage`, `timeLimitMs`, `memoryLimitMb` +- Contains `List` — each with `testCaseId`, `inputPath`, `outputPath` + +### Producer — TestGenerationResultProducer +Publishes TestGenerationResult to `codehive_test_generation_result_queue`. + +Outgoing payload: `model/dto/queue/TestGenerationResult` +- `assignmentId`, `success`, `generatedCount`, `errorMessage` ## Operational Notes -- Backend and worker must share queue names and compatible DTO schemas. -- Messaging logs are the first place to debug job handoff issues. +- Backend and worker must share queue names and compatible DTO schemas — both projects maintain mirrored copies of queue DTOs. +- `[WORKFLOW]`-prefixed log lines trace message flow end to end. +- Use messaging logs as the first stop when debugging job handoff issues. - Keep payload evolution coordinated across both services. From afcbde5b73f17a91b1f8611479b4caf2c4ef9068 Mon Sep 17 00:00:00 2001 From: IrminDev Date: Fri, 8 May 2026 22:58:27 -0600 Subject: [PATCH 16/32] Added useful files --- codehive-testing-files/requests/README.md | 180 ++++++++++++++++++ .../requests/http/01_login.http | 19 ++ .../requests/http/02_create_assignment.sh | 51 +++++ .../requests/http/03_practice_ac.http | 43 +++++ .../requests/http/04_definitive_ac.http | 45 +++++ .../requests/http/05_poll_execution.http | 24 +++ .../requests/http/06_tle_execution.http | 44 +++++ .../requests/http/07_wa_execution.http | 50 +++++ .../requests/solutions/Main.java | 35 ++++ .../requests/solutions/reference_solution.c | 42 ++++ .../requests/solutions/solution.cpp | 38 ++++ .../requests/solutions/solution.py | 26 +++ .../requests/solutions/tle_solution.c | 47 +++++ .../requests/solutions/wa_solution.c | 45 +++++ .../test_cases/generate_large_input.py | 29 +++ .../requests/test_cases/input1.txt | 3 + .../requests/test_cases/input2.txt | 3 + .../requests/test_cases/input3.txt | 3 + .../requests/test_cases/input4.txt | 3 + .../requests/test_cases/input5.txt | 3 + 20 files changed, 733 insertions(+) create mode 100644 codehive-testing-files/requests/README.md create mode 100644 codehive-testing-files/requests/http/01_login.http create mode 100755 codehive-testing-files/requests/http/02_create_assignment.sh create mode 100644 codehive-testing-files/requests/http/03_practice_ac.http create mode 100644 codehive-testing-files/requests/http/04_definitive_ac.http create mode 100644 codehive-testing-files/requests/http/05_poll_execution.http create mode 100644 codehive-testing-files/requests/http/06_tle_execution.http create mode 100644 codehive-testing-files/requests/http/07_wa_execution.http create mode 100644 codehive-testing-files/requests/solutions/Main.java create mode 100644 codehive-testing-files/requests/solutions/reference_solution.c create mode 100644 codehive-testing-files/requests/solutions/solution.cpp create mode 100644 codehive-testing-files/requests/solutions/solution.py create mode 100644 codehive-testing-files/requests/solutions/tle_solution.c create mode 100644 codehive-testing-files/requests/solutions/wa_solution.c create mode 100644 codehive-testing-files/requests/test_cases/generate_large_input.py create mode 100644 codehive-testing-files/requests/test_cases/input1.txt create mode 100644 codehive-testing-files/requests/test_cases/input2.txt create mode 100644 codehive-testing-files/requests/test_cases/input3.txt create mode 100644 codehive-testing-files/requests/test_cases/input4.txt create mode 100644 codehive-testing-files/requests/test_cases/input5.txt diff --git a/codehive-testing-files/requests/README.md b/codehive-testing-files/requests/README.md new file mode 100644 index 0000000..571781f --- /dev/null +++ b/codehive-testing-files/requests/README.md @@ -0,0 +1,180 @@ +# CodeHive — Testing Files + +End-to-end testing materials for the **Maximum Sum of Sliding Window** demo assignment. Covers assignment creation, practice runs, definitive submissions, and the full verdict range (AC, TLE, WA). + +--- + +## Problem Statement + +**Maximum Sum of Sliding Window** + +Given an array of `N` integers and a window size `K`, find the maximum sum of any contiguous subarray of length exactly `K`. + +**Input format** +``` +N +a[0] a[1] ... a[N-1] +K +``` + +**Output format** +``` + +``` + +**Constraints** +- `1 ≤ N ≤ 200 000` +- `1 ≤ K ≤ N` +- `-10^4 ≤ a[i] ≤ 10^4` + +**Assignment limits:** time 1 000 ms · memory 256 MB + +--- + +## Directory Layout + +``` +requests/ +├── README.md ← this file +├── solutions/ +│ ├── reference_solution.c ← O(n) sliding window (reference, C) +│ ├── solution.cpp ← same algorithm in C++ +│ ├── solution.py ← same algorithm in Python +│ ├── Main.java ← same algorithm in Java (class Main) +│ ├── tle_solution.c ← O(n*k) brute-force — triggers TLE +│ └── wa_solution.c ← max_sum=0 bug — fails all-negative input +├── test_cases/ +│ ├── input1.txt ← sample (visible to students) +│ ├── input2.txt ← hidden +│ ├── input3.txt ← hidden — all-negative array +│ ├── input4.txt ← hidden — single element +│ ├── input5.txt ← hidden — mixed values +│ └── generate_large_input.py ← generates N=200000, K=100000 stress input +└── http/ + ├── 01_login.http ← obtain JWT token + ├── 02_create_assignment.sh ← curl multipart upload (teacher) + ├── 03_practice_ac.http ← PRACTICE mode, Python AC + ├── 04_definitive_ac.http ← DEFINITIVE mode, Java AC + ├── 05_poll_execution.http ← poll any execution by ID + ├── 06_tle_execution.http ← DEFINITIVE mode, C brute-force TLE + └── 07_wa_execution.http ← PRACTICE mode, Python WA (all-negative) +``` + +--- + +## Solutions + +| File | Language | Algorithm | Expected verdict | +|------|----------|-----------|-----------------| +| `reference_solution.c` | C | O(n) sliding window | AC — used as reference | +| `solution.cpp` | C++ | O(n) sliding window | AC | +| `solution.py` | Python | O(n) sliding window | AC | +| `Main.java` | Java | O(n) sliding window | AC | +| `tle_solution.c` | C | O(n·k) brute-force | TLE on large input | +| `wa_solution.c` | C | Buggy (`maxSum = 0`) | WA on all-negative input | + +### Why `tle_solution.c` is slow + +The brute-force solution recomputes the sum of each window by iterating over all `k` elements every step. With `N=200 000` and `K=100 000` this is ≈ 10^10 operations — roughly 10 000× over the 1 000 ms budget. The O(n) solution maintains a running sum with a single add and subtract per step. + +### Why `wa_solution.c` is wrong + +The variable `maxSum` is initialised to `0` rather than to the value of the first window. Any input whose maximum window sum is negative produces output `0` instead of the correct negative value. The all-negative test case (`input3.txt`) exposes this bug: + +``` +Input: 6 elements [-1, -2, -3, -4, -5, -6], k=2 +Correct output: -3 (window [-1, -2]) +Buggy output: 0 +``` + +--- + +## Test Cases + +| File | N | Array | K | Expected answer | Notes | +|------|---|-------|---|-----------------|-------| +| `input1.txt` | 5 | 1 2 3 4 5 | 2 | **9** | sample — visible to students | +| `input2.txt` | 8 | 2 1 5 1 3 2 6 2 | 3 | **11** | window [3, 2, 6] | +| `input3.txt` | 6 | -1 -2 -3 -4 -5 -6 | 2 | **-3** | all-negative — catches WA bug | +| `input4.txt` | 1 | 7 | 1 | **7** | single-element edge case | +| `input5.txt` | 10 | 3 1 4 1 5 9 2 6 5 3 | 4 | **22** | window [5, 9, 2, 6] | + +### Stress / TLE test case + +Generate a large input that triggers TLE in the brute-force solution: + +```bash +cd test_cases +python generate_large_input.py # → input5_large.txt (N=200000, K=100000) +python generate_large_input.py 50000 25000 # custom N and K +``` + +Expected answer for the default parameters: **100 000** (all values are 1). + +To use this as a hidden test case, re-run `02_create_assignment.sh` and add the generated file as an additional `-F "testCaseInputs=@..."` argument. + +--- + +## HTTP Request Files + +### Prerequisites + +- Backend running at `http://localhost:8080` +- RabbitMQ and MinIO running (default Docker Compose setup) +- A teacher or admin account exists + +### Step-by-step workflow + +#### 1. Login (`01_login.http`) + +Open in VS Code REST Client (or IntelliJ HTTP Client). Send the `POST /api/auth/login` request. Copy the `token` field from the response. + +#### 2. Create assignment (`02_create_assignment.sh`) + +```bash +cd codehive-testing-files/requests +TOKEN="" bash http/02_create_assignment.sh +``` + +The script uploads `reference_solution.c` + all 5 input files in a single `multipart/form-data` request. The server responds with **HTTP 202** and a JSON body containing the assignment `id`. Copy that UUID — you will need it for steps 3–7. + +After the request returns, the backend publishes a `TestGenerationJob` to RabbitMQ. The worker compiles the reference solution, runs it against each input, and writes the expected outputs to MinIO. Once all outputs are ready the assignment is activated (`isActive=true`). This normally takes a few seconds; wait for it before sending DEFINITIVE submissions. + +#### 3. Practice run — AC (`03_practice_ac.http`) + +Set `@token` and `@assignmentId`. Send the request. The server returns **HTTP 202** with an `executionId`. Poll with `05_poll_execution.http`. Expected: both test cases → `AC`. + +#### 4. Definitive submission — AC (`04_definitive_ac.http`) + +Set `@token` and `@assignmentId`. Send the request. Poll with `05_poll_execution.http`. Expected: all 5 test cases → `AC`. + +#### 5. Poll execution (`05_poll_execution.http`) + +Set `@token` and `@executionId` (from any previous submission response). Keep sending until `status` leaves `PENDING` / `RUNNING`. The final `COMPLETED` response contains per-test-case verdicts. + +#### 6. Definitive submission — TLE (`06_tle_execution.http`) + +Triggers TLE on any large hidden test case. All small test cases still pass (brute-force is correct, just slow). Expected: small inputs → `AC`, large input → `TLE`. + +#### 7. Practice run — WA (`07_wa_execution.http`) + +Two inline test cases are sent. The all-negative one exposes the `max_sum = 0` initialisation bug. Expected: test case 1 → `WA` (got `0`, expected `-3`), test case 2 → `AC`. + +--- + +## Execution Modes + +| Mode | `executionType` | Test cases source | Use case | +|------|----------------|-------------------|----------| +| PRACTICE | `"PRACTICE"` | Inline in request body (`testCases` list) | Student exploring the problem | +| DEFINITIVE | `"DEFINITIVE"` | Pre-generated outputs in MinIO | Official graded submission | + +In PRACTICE mode the worker fetches the reference solution path from the assignment, runs it on each inline test case, and compares outputs. In DEFINITIVE mode the worker counts the pre-generated test cases and compares student output against stored expected outputs. + +--- + +## Extending the Test Suite + +1. Add more `.txt` input files to `test_cases/`. +2. Modify `02_create_assignment.sh` to upload them alongside the existing ones. +3. Adjust `"sampleFlags"` in the metadata JSON to mark which inputs are visible to students (`true`) versus hidden (`false`). The list length must match the number of uploaded test-case files. diff --git a/codehive-testing-files/requests/http/01_login.http b/codehive-testing-files/requests/http/01_login.http new file mode 100644 index 0000000..1000c87 --- /dev/null +++ b/codehive-testing-files/requests/http/01_login.http @@ -0,0 +1,19 @@ +# ============================================================ +# STEP 1 — Login and obtain a JWT token +# +# Copy the `token` field from the response body and set the +# @token variable at the top of the other .http files. +# +# Compatible with: VS Code REST Client, IntelliJ HTTP Client +# ============================================================ + +@baseUrl = http://localhost:8080 + +### Login as teacher / admin +POST {{baseUrl}}/api/auth/login +Content-Type: application/json + +{ + "identifier": "teacher@codehive.com", + "password": "your-password-here" +} diff --git a/codehive-testing-files/requests/http/02_create_assignment.sh b/codehive-testing-files/requests/http/02_create_assignment.sh new file mode 100755 index 0000000..d8c54cb --- /dev/null +++ b/codehive-testing-files/requests/http/02_create_assignment.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# ============================================================ +# STEP 2 — Create an assignment (teacher / admin only) +# +# Prerequisites: +# 1. Run 01_login.http first and set TOKEN below. +# 2. Run this script from the requests/ directory so that +# relative paths to solutions/ and test_cases/ resolve. +# +# Usage: +# cd codehive-testing-files/requests +# TOKEN="" bash http/02_create_assignment.sh +# +# On success the server returns HTTP 202 with a JSON body that +# contains the assignment UUID — copy it into the @assignmentId +# variable at the top of the other .http files. +# ============================================================ + +BASE_URL="${BASE_URL:-http://localhost:8080}" +TOKEN="${TOKEN:?Please set TOKEN to a valid JWT}" + +METADATA='{ + "title": "Maximum Sum of Sliding Window", + "description": "Given N integers and window size K, find the maximum sum of any contiguous subarray of length K.", + "timeLimitMs": 1000, + "memoryLimitMb": 256, + "comparatorType": "EXACT_MATCH", + "allowedLanguages": ["JAVA", "PYTHON", "C", "CPP"], + "referenceLanguage": "C", + "dueDate": "2025-12-31T23:59:59", + "constraints": ["1 <= N <= 200000", "1 <= K <= N", "-10^4 <= nums[i] <= 10^4"], + "hints": ["Consider using a sliding window technique"], + "tags": ["sliding window", "arrays"], + "sampleFlags": [true, false, false, false, false] +}' + +# sampleFlags: input1 is a sample (visible to students); inputs 2-5 are hidden + +curl -s -X POST "${BASE_URL}/api/assignments" \ + -H "Authorization: Bearer ${TOKEN}" \ + -F "metadata=${METADATA};type=application/json" \ + -F "referenceSolution=@solutions/reference_solution.c;type=text/plain" \ + -F "testCaseInputs=@test_cases/input1.txt;type=text/plain" \ + -F "testCaseInputs=@test_cases/input2.txt;type=text/plain" \ + -F "testCaseInputs=@test_cases/input3.txt;type=text/plain" \ + -F "testCaseInputs=@test_cases/input4.txt;type=text/plain" \ + -F "testCaseInputs=@test_cases/input5.txt;type=text/plain" \ + | python3 -m json.tool + +echo "" +echo "Copy the 'id' field from the response above and set it as assignmentId in the .http files." diff --git a/codehive-testing-files/requests/http/03_practice_ac.http b/codehive-testing-files/requests/http/03_practice_ac.http new file mode 100644 index 0000000..e1aa5ef --- /dev/null +++ b/codehive-testing-files/requests/http/03_practice_ac.http @@ -0,0 +1,43 @@ +# ============================================================ +# STEP 3 — PRACTICE execution: Python AC solution +# +# Mode: PRACTICE +# Language: PYTHON +# Verdict: AC (Accepted) on both inline test cases +# +# In PRACTICE mode the client provides its own test case +# strings. The worker runs the reference solution on each +# one and compares outputs. No assignment test cases are used. +# +# Set @token from 01_login.http response. +# assignmentId is optional for PRACTICE but recommended so the +# worker can apply the assignment's time/memory limits. +# +# Compatible with: VS Code REST Client, IntelliJ HTTP Client +# ============================================================ + +@baseUrl = http://localhost:8080 +@token = +@assignmentId = + +### Practice run 1 — basic case (n=5, k=2, expected 9) +POST {{baseUrl}}/api/execution/check +Content-Type: application/json +Authorization: Bearer {{token}} + +{ + "code": "import sys\n\ndef main():\n data = sys.stdin.read().split()\n n = int(data[0])\n nums = [int(data[i + 1]) for i in range(n)]\n k = int(data[n + 1])\n if k > n or k <= 0:\n print(0)\n return\n window = sum(nums[:k])\n max_sum = window\n for i in range(k, n):\n window += nums[i] - nums[i - k]\n if window > max_sum:\n max_sum = window\n print(max_sum)\n\nmain()", + "language": "PYTHON", + "assignmentId": "{{assignmentId}}", + "executionType": "PRACTICE", + "testCases": [ + "5\n1 2 3 4 5\n2", + "8\n2 1 5 1 3 2 6 2\n3" + ] +} + +### +# Expected result after polling 05_poll_execution.http: +# status: COMPLETED +# testCaseResults[0]: verdict=AC, output="9" (window [4,5]) +# testCaseResults[1]: verdict=AC, output="11" (window [3,2,6]) diff --git a/codehive-testing-files/requests/http/04_definitive_ac.http b/codehive-testing-files/requests/http/04_definitive_ac.http new file mode 100644 index 0000000..f738bb9 --- /dev/null +++ b/codehive-testing-files/requests/http/04_definitive_ac.http @@ -0,0 +1,45 @@ +# ============================================================ +# STEP 4 — DEFINITIVE execution: Java AC solution +# +# Mode: DEFINITIVE (official submission) +# Language: JAVA +# Verdict: AC (Accepted) on all 5 assignment test cases +# +# In DEFINITIVE mode the worker fetches pre-generated expected +# outputs from MinIO (stored during assignment creation) and +# compares each one against the student's output. +# +# The assignment must be active (isActive=true) before +# submitting in DEFINITIVE mode. Wait for the worker to +# finish test-case generation after step 2. +# +# Set @token from 01_login.http response. +# Set @assignmentId from 02_create_assignment.sh response. +# +# Compatible with: VS Code REST Client, IntelliJ HTTP Client +# ============================================================ + +@baseUrl = http://localhost:8080 +@token = +@assignmentId = + +### Definitive submission — Java sliding-window O(n) solution +POST {{baseUrl}}/api/execution/check +Content-Type: application/json +Authorization: Bearer {{token}} + +{ + "code": "import java.util.*;\npublic class Main {\n public static void main(String[] args) {\n Scanner sc = new Scanner(System.in);\n int n = sc.nextInt();\n int[] nums = new int[n];\n for (int i = 0; i < n; i++) nums[i] = sc.nextInt();\n int k = sc.nextInt();\n sc.close();\n if (k > n || k <= 0) { System.out.println(0); return; }\n long window = 0;\n for (int i = 0; i < k; i++) window += nums[i];\n long maxSum = window;\n for (int i = k; i < n; i++) {\n window += nums[i] - nums[i - k];\n if (window > maxSum) maxSum = window;\n }\n System.out.println(maxSum);\n }\n}", + "language": "JAVA", + "assignmentId": "{{assignmentId}}", + "executionType": "DEFINITIVE" +} + +### +# Expected result after polling 05_poll_execution.http: +# status: COMPLETED +# testCaseResults[0]: verdict=AC input1 — n=5, expected 9 +# testCaseResults[1]: verdict=AC input2 — n=8, expected 11 +# testCaseResults[2]: verdict=AC input3 — all-negative, expected -3 +# testCaseResults[3]: verdict=AC input4 — single element, expected 7 +# testCaseResults[4]: verdict=AC input5 — n=10, expected 25 diff --git a/codehive-testing-files/requests/http/05_poll_execution.http b/codehive-testing-files/requests/http/05_poll_execution.http new file mode 100644 index 0000000..047736f --- /dev/null +++ b/codehive-testing-files/requests/http/05_poll_execution.http @@ -0,0 +1,24 @@ +# ============================================================ +# STEP 5 — Poll execution status +# +# After submitting an execution request (steps 3, 4, 6, or 7) +# copy the returned `id` into @executionId below and send +# this request repeatedly until `status` is no longer PENDING +# or RUNNING. +# +# Possible statuses: +# PENDING — queued, not yet picked up by the worker +# RUNNING — worker is compiling/executing +# COMPLETED — all test cases finished; check individual verdicts +# ERROR — internal worker error +# +# Compatible with: VS Code REST Client, IntelliJ HTTP Client +# ============================================================ + +@baseUrl = http://localhost:8080 +@token = +@executionId = + +### Get execution result +GET {{baseUrl}}/api/execution/check/{{executionId}} +Authorization: Bearer {{token}} diff --git a/codehive-testing-files/requests/http/06_tle_execution.http b/codehive-testing-files/requests/http/06_tle_execution.http new file mode 100644 index 0000000..8c63a34 --- /dev/null +++ b/codehive-testing-files/requests/http/06_tle_execution.http @@ -0,0 +1,44 @@ +# ============================================================ +# STEP 6 — DEFINITIVE execution: C TLE solution (brute-force) +# +# Mode: DEFINITIVE +# Language: C +# Verdict: TLE (Time Limit Exceeded) on large test case +# +# The brute-force O(n*k) solution recomputes each window from +# scratch. With N=200000 and K=100000 it performs ~10^10 +# iterations — far beyond the 1000 ms time limit. +# +# Test case 5 (input5.txt, n=10, k=4) will likely pass, but +# if you also upload the large test case generated by +# test_cases/generate_large_input.py (input5_large.txt) as an +# additional hidden test case, TLE becomes obvious. +# +# Set @token from 01_login.http response. +# Set @assignmentId from 02_create_assignment.sh response. +# +# Compatible with: VS Code REST Client, IntelliJ HTTP Client +# ============================================================ + +@baseUrl = http://localhost:8080 +@token = +@assignmentId = + +### Definitive submission — C O(n*k) brute-force TLE solution +POST {{baseUrl}}/api/execution/check +Content-Type: application/json +Authorization: Bearer {{token}} + +{ + "code": "#include \nint main() {\n int n;\n scanf(\"%d\", &n);\n int nums[200005];\n for (int i = 0; i < n; i++) scanf(\"%d\", &nums[i]);\n int k;\n scanf(\"%d\", &k);\n if (k > n || k <= 0) { printf(\"0\\n\"); return 0; }\n long long maxSum = 0;\n int initialized = 0;\n for (int i = 0; i <= n - k; i++) {\n long long sum = 0;\n for (int j = i; j < i + k; j++) sum += nums[j];\n if (!initialized || sum > maxSum) { maxSum = sum; initialized = 1; }\n }\n printf(\"%lld\\n\", maxSum);\n return 0;\n}", + "language": "C", + "assignmentId": "{{assignmentId}}", + "executionType": "DEFINITIVE" +} + +### +# Expected result after polling 05_poll_execution.http +# (assuming a large test case is present): +# status: COMPLETED +# testCaseResults[small cases]: verdict=AC (brute-force is correct) +# testCaseResults[large case]: verdict=TLE (exceeds 1000 ms limit) diff --git a/codehive-testing-files/requests/http/07_wa_execution.http b/codehive-testing-files/requests/http/07_wa_execution.http new file mode 100644 index 0000000..f77f0e3 --- /dev/null +++ b/codehive-testing-files/requests/http/07_wa_execution.http @@ -0,0 +1,50 @@ +# ============================================================ +# STEP 7 — PRACTICE execution: Python WA solution +# +# Mode: PRACTICE +# Language: PYTHON +# Verdict: WA (Wrong Answer) on the all-negative test case +# +# The buggy solution initialises max_sum = 0 instead of +# max_sum = window. It produces the correct answer whenever +# the maximum sum is positive, but returns 0 for arrays whose +# every subarray sum is negative. +# +# Test case used here: "6\n-1 -2 -3 -4 -5 -6\n2" +# Correct answer : -3 (window [-1, -2]) +# Buggy output : 0 → WA +# +# A second test case with positive values is also included to +# show the solution passes when the answer is non-negative. +# +# Set @token from 01_login.http response. +# assignmentId is optional for PRACTICE but recommended. +# +# Compatible with: VS Code REST Client, IntelliJ HTTP Client +# ============================================================ + +@baseUrl = http://localhost:8080 +@token = +@assignmentId = + +### Practice run — Python WA solution (max_sum = 0 bug) +POST {{baseUrl}}/api/execution/check +Content-Type: application/json +Authorization: Bearer {{token}} + +{ + "code": "import sys\n\ndef main():\n data = sys.stdin.read().split()\n n = int(data[0])\n nums = [int(data[i + 1]) for i in range(n)]\n k = int(data[n + 1])\n if k > n or k <= 0:\n print(0)\n return\n window = sum(nums[:k])\n max_sum = 0\n if window > max_sum:\n max_sum = window\n for i in range(k, n):\n window += nums[i] - nums[i - k]\n if window > max_sum:\n max_sum = window\n print(max_sum)\n\nmain()", + "language": "PYTHON", + "assignmentId": "{{assignmentId}}", + "executionType": "PRACTICE", + "testCases": [ + "6\n-1 -2 -3 -4 -5 -6\n2", + "5\n1 2 3 4 5\n2" + ] +} + +### +# Expected result after polling 05_poll_execution.http: +# status: COMPLETED +# testCaseResults[0]: verdict=WA got="0" expected="-3" +# testCaseResults[1]: verdict=AC got="9" expected="9" diff --git a/codehive-testing-files/requests/solutions/Main.java b/codehive-testing-files/requests/solutions/Main.java new file mode 100644 index 0000000..c9485b7 --- /dev/null +++ b/codehive-testing-files/requests/solutions/Main.java @@ -0,0 +1,35 @@ +/* + * CORRECT SOLUTION — Java + * Same sliding window logic as the C reference solution. + * File must be named Main.java — the worker compiles with: javac Main.java + */ +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + + int n = sc.nextInt(); + int[] nums = new int[n]; + for (int i = 0; i < n; i++) nums[i] = sc.nextInt(); + + int k = sc.nextInt(); + sc.close(); + + if (k > n || k <= 0) { + System.out.println(0); + return; + } + + long window = 0; + for (int i = 0; i < k; i++) window += nums[i]; + + long maxSum = window; + for (int i = k; i < n; i++) { + window += nums[i] - nums[i - k]; + if (window > maxSum) maxSum = window; + } + + System.out.println(maxSum); + } +} diff --git a/codehive-testing-files/requests/solutions/reference_solution.c b/codehive-testing-files/requests/solutions/reference_solution.c new file mode 100644 index 0000000..372c5d0 --- /dev/null +++ b/codehive-testing-files/requests/solutions/reference_solution.c @@ -0,0 +1,42 @@ +/* + * REFERENCE SOLUTION (Professor's) + * Problem: Maximum Sum of Sliding Window of size K + * + * Input: + * Line 1: N (number of elements) + * Line 2: N space-separated integers + * Line 3: K (window size) + * Output: + * Maximum sum of any contiguous subarray of length K + * + * Strategy: O(n) sliding window — add next element, remove leftmost. + */ +#include + +int main() { + int n; + scanf("%d", &n); + + int nums[200005]; + for (int i = 0; i < n; i++) scanf("%d", &nums[i]); + + int k; + scanf("%d", &k); + + if (k > n || k <= 0) { + printf("0\n"); + return 0; + } + + long long window = 0; + for (int i = 0; i < k; i++) window += nums[i]; + + long long maxSum = window; + for (int i = k; i < n; i++) { + window += nums[i] - nums[i - k]; + if (window > maxSum) maxSum = window; + } + + printf("%lld\n", maxSum); + return 0; +} diff --git a/codehive-testing-files/requests/solutions/solution.cpp b/codehive-testing-files/requests/solutions/solution.cpp new file mode 100644 index 0000000..11e0f3e --- /dev/null +++ b/codehive-testing-files/requests/solutions/solution.cpp @@ -0,0 +1,38 @@ +/* + * CORRECT SOLUTION — C++ + * Same sliding window logic as the C reference solution. + */ +#include +#include +using namespace std; + +int main() { + ios::sync_with_stdio(false); + cin.tie(nullptr); + + int n; + cin >> n; + + vector nums(n); + for (int i = 0; i < n; i++) cin >> nums[i]; + + int k; + cin >> k; + + if (k > n || k <= 0) { + cout << 0 << endl; + return 0; + } + + long long window = 0; + for (int i = 0; i < k; i++) window += nums[i]; + + long long maxSum = window; + for (int i = k; i < n; i++) { + window += nums[i] - nums[i - k]; + if (window > maxSum) maxSum = window; + } + + cout << maxSum << endl; + return 0; +} diff --git a/codehive-testing-files/requests/solutions/solution.py b/codehive-testing-files/requests/solutions/solution.py new file mode 100644 index 0000000..f21409e --- /dev/null +++ b/codehive-testing-files/requests/solutions/solution.py @@ -0,0 +1,26 @@ +# CORRECT SOLUTION — Python +# Same sliding window logic as the C reference solution. + +import sys + +def main(): + data = sys.stdin.read().split() + n = int(data[0]) + nums = [int(data[i + 1]) for i in range(n)] + k = int(data[n + 1]) + + if k > n or k <= 0: + print(0) + return + + window = sum(nums[:k]) + max_sum = window + + for i in range(k, n): + window += nums[i] - nums[i - k] + if window > max_sum: + max_sum = window + + print(max_sum) + +main() diff --git a/codehive-testing-files/requests/solutions/tle_solution.c b/codehive-testing-files/requests/solutions/tle_solution.c new file mode 100644 index 0000000..5a2a3e7 --- /dev/null +++ b/codehive-testing-files/requests/solutions/tle_solution.c @@ -0,0 +1,47 @@ +/* + * TLE SOLUTION — intentional O(n * k) brute force + * + * Instead of maintaining a running sum, this solution recomputes + * each window from scratch — O(k) per window, O(n*k) total. + * + * For N=200000 and K=100000: + * iterations ≈ 100000 × 100000 = 10^10 → far exceeds 1000 ms limit + * + * Expected verdict: TLE on test case 5 (large input). + * Will produce correct output on small test cases (1–4). + */ +#include + +int main() { + int n; + scanf("%d", &n); + + int nums[200005]; + for (int i = 0; i < n; i++) scanf("%d", &nums[i]); + + int k; + scanf("%d", &k); + + if (k > n || k <= 0) { + printf("0\n"); + return 0; + } + + long long maxSum = 0; + int initialized = 0; + + for (int i = 0; i <= n - k; i++) { + long long sum = 0; + /* Recompute the full window every time — no sliding */ + for (int j = i; j < i + k; j++) { + sum += nums[j]; + } + if (!initialized || sum > maxSum) { + maxSum = sum; + initialized = 1; + } + } + + printf("%lld\n", maxSum); + return 0; +} diff --git a/codehive-testing-files/requests/solutions/wa_solution.c b/codehive-testing-files/requests/solutions/wa_solution.c new file mode 100644 index 0000000..86fe851 --- /dev/null +++ b/codehive-testing-files/requests/solutions/wa_solution.c @@ -0,0 +1,45 @@ +/* + * WA SOLUTION — incorrect initialisation of maxSum + * + * Bug: maxSum is initialised to 0 instead of the first window sum. + * When every possible window has a strictly negative sum, this + * solution outputs 0 instead of the actual maximum (which is also + * negative). + * + * Affected test cases: + * - input3.txt: [-1,-2,-3,-4,-5,-6], K=2 → outputs 0, expected -3 + * + * All other test cases (positive windows) still pass. + * Expected verdict: WA. + */ +#include + +int main() { + int n; + scanf("%d", &n); + + int nums[200005]; + for (int i = 0; i < n; i++) scanf("%d", &nums[i]); + + int k; + scanf("%d", &k); + + if (k > n || k <= 0) { + printf("0\n"); + return 0; + } + + long long window = 0; + for (int i = 0; i < k; i++) window += nums[i]; + + long long maxSum = 0; /* BUG: should be `window`, not 0 */ + if (window > maxSum) maxSum = window; + + for (int i = k; i < n; i++) { + window += nums[i] - nums[i - k]; + if (window > maxSum) maxSum = window; + } + + printf("%lld\n", maxSum); + return 0; +} diff --git a/codehive-testing-files/requests/test_cases/generate_large_input.py b/codehive-testing-files/requests/test_cases/generate_large_input.py new file mode 100644 index 0000000..1acf003 --- /dev/null +++ b/codehive-testing-files/requests/test_cases/generate_large_input.py @@ -0,0 +1,29 @@ +""" +Generates a large test case that triggers TLE on the O(n*k) brute-force solution. + +Parameters: + N = 200000 (elements) + K = 100000 (window size) + +The brute-force iterates O(n * k) ≈ 10^10 times — far beyond the 1000 ms limit. +The O(n) sliding-window reference solution finishes in < 50 ms. + +Expected answer: 100000 (all values are 1, so every window sums to K = 100000). + +Usage: + python generate_large_input.py # writes to input5_large.txt + python generate_large_input.py 50000 25000 # custom N and K +""" +import sys + +def generate(n: int = 200_000, k: int = 100_000, filename: str = "input5_large.txt"): + with open(filename, "w") as f: + f.write(f"{n}\n") + f.write(" ".join(["1"] * n) + "\n") + f.write(f"{k}\n") + print(f"Written {filename} (N={n}, K={k}, expected answer={k})") + +if __name__ == "__main__": + n = int(sys.argv[1]) if len(sys.argv) > 1 else 200_000 + k = int(sys.argv[2]) if len(sys.argv) > 2 else 100_000 + generate(n, k) diff --git a/codehive-testing-files/requests/test_cases/input1.txt b/codehive-testing-files/requests/test_cases/input1.txt new file mode 100644 index 0000000..053ed15 --- /dev/null +++ b/codehive-testing-files/requests/test_cases/input1.txt @@ -0,0 +1,3 @@ +5 +1 2 3 4 5 +2 diff --git a/codehive-testing-files/requests/test_cases/input2.txt b/codehive-testing-files/requests/test_cases/input2.txt new file mode 100644 index 0000000..de1bafd --- /dev/null +++ b/codehive-testing-files/requests/test_cases/input2.txt @@ -0,0 +1,3 @@ +8 +2 1 5 1 3 2 6 2 +3 diff --git a/codehive-testing-files/requests/test_cases/input3.txt b/codehive-testing-files/requests/test_cases/input3.txt new file mode 100644 index 0000000..91709ef --- /dev/null +++ b/codehive-testing-files/requests/test_cases/input3.txt @@ -0,0 +1,3 @@ +6 +-1 -2 -3 -4 -5 -6 +2 diff --git a/codehive-testing-files/requests/test_cases/input4.txt b/codehive-testing-files/requests/test_cases/input4.txt new file mode 100644 index 0000000..2d5007b --- /dev/null +++ b/codehive-testing-files/requests/test_cases/input4.txt @@ -0,0 +1,3 @@ +1 +7 +1 diff --git a/codehive-testing-files/requests/test_cases/input5.txt b/codehive-testing-files/requests/test_cases/input5.txt new file mode 100644 index 0000000..f0e3324 --- /dev/null +++ b/codehive-testing-files/requests/test_cases/input5.txt @@ -0,0 +1,3 @@ +10 +3 1 4 1 5 9 2 6 5 3 +4 From 88f04ae7457c22e1a1cf9356a7c058037cedc8f1 Mon Sep 17 00:00:00 2001 From: IrminDev Date: Sat, 9 May 2026 00:08:45 -0600 Subject: [PATCH 17/32] Minor fixes --- codehive-backend/docker-compose.yaml | 56 ++++++++++++------- .../codehive/config/AdminInitializer.java | 27 +++------ .../codehive/config/SecurityConfig.java | 5 +- .../controller/AssignmentController.java | 2 +- .../src/main/resources/application.properties | 9 ++- .../worker/config/RabbitMQConfig.java | 25 ++++++--- .../producer/ExecutionResultProducer.java | 13 +++-- .../TestGenerationResultProducer.java | 7 ++- .../src/main/resources/application.properties | 12 ++-- 9 files changed, 95 insertions(+), 61 deletions(-) diff --git a/codehive-backend/docker-compose.yaml b/codehive-backend/docker-compose.yaml index e6c211a..4ef8113 100644 --- a/codehive-backend/docker-compose.yaml +++ b/codehive-backend/docker-compose.yaml @@ -1,36 +1,54 @@ services: postgres: - image: 'postgres:18.1-alpine' - container_name: 'db' + image: "postgres:18.1-alpine" + container_name: "db" environment: - - 'POSTGRES_DB=${DATABASE_NAME}' - - 'POSTGRES_PASSWORD=${DATABASE_PASSWORD}' - - 'POSTGRES_USER=${DATABASE_USERNAME}' + - "POSTGRES_DB=${DATABASE_NAME}" + - "POSTGRES_PASSWORD=${DATABASE_PASSWORD}" + - "POSTGRES_USER=${DATABASE_USERNAME}" ports: - - '5432:5432' + - "5432:5432" volumes: - - 'db_data:/var/lib/postgresql/data' + - "db_data:/var/lib/postgresql/data" rabbitmq: - image: 'rabbitmq:4.2.2-management-alpine' - container_name: 'rabbitmq' + image: "rabbitmq:4.2.2-management-alpine" + container_name: "rabbitmq" ports: - - '5672:5672' - - '15672:15672' + - "5672:5672" + - "15672:15672" minio: - image: 'minio/minio:RELEASE.2025-09-07T16-13-09Z-cpuv1' - container_name: 'minio' + image: "minio/minio:RELEASE.2025-09-07T16-13-09Z-cpuv1" + container_name: "minio" command: 'server /data --console-address ":9001"' ports: - - '9000:9000' - - '9001:9001' + - "9000:9000" + - "9001:9001" environment: - - 'MINIO_ROOT_USER=${MINIO_ROOT_USER}' - - 'MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}' + - "MINIO_ROOT_USER=${MINIO_ACCESS_KEY}" + - "MINIO_ROOT_PASSWORD=${MINIO_SECRET_KEY}" volumes: - - 'minio_data:/data' + - "minio_data:/data" + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 5s + timeout: 5s + retries: 5 + + minio-init: + image: "minio/mc:latest" + container_name: "minio-init" + depends_on: + minio: + condition: service_healthy + entrypoint: > + /bin/sh -c " + mc alias set local http://minio:9000 ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}; + mc mb --ignore-existing local/${MINIO_BUCKET_NAME}; + exit 0; + " volumes: db_data: - minio_data: \ No newline at end of file + minio_data: diff --git a/codehive-backend/src/main/java/com/github/codehive/config/AdminInitializer.java b/codehive-backend/src/main/java/com/github/codehive/config/AdminInitializer.java index c45c8aa..b355551 100644 --- a/codehive-backend/src/main/java/com/github/codehive/config/AdminInitializer.java +++ b/codehive-backend/src/main/java/com/github/codehive/config/AdminInitializer.java @@ -1,7 +1,5 @@ package com.github.codehive.config; -import java.util.List; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -25,7 +23,7 @@ public class AdminInitializer implements CommandLineRunner { @Value("${app.admin.email:admin@codehive.com}") private String adminEmail; - @Value("${app.admin.password}") + @Value("${app.admin.password:Admin@12345}") private String adminPassword; @Value("${app.admin.name:Super}") @@ -49,22 +47,13 @@ public void run(String... args) { return; } - User admin = new User(); - admin.setEmail(adminEmail); - admin.setPassword(passwordEncoder.encode(adminPassword)); - admin.setName(adminName); - admin.setLastName(adminLastName); - admin.setEnrollmentNumber(adminEnrollmentNumber); - admin.setRole(Role.ADMIN); - admin.setIsActive(true); - admin.setTemporaryPassword(false); - admin.setScopes(List.of( - Scope.SUPER_ADMIN, - Scope.MANAGE_USERS, - Scope.MANAGE_GROUPS, - Scope.CHECK_ANALYTICS, - Scope.CREATE_GROUP - )); + User admin = new User(adminName, adminLastName, adminEnrollmentNumber, adminEmail, + passwordEncoder.encode(adminPassword), Role.ADMIN); + admin.addScope(Scope.SUPER_ADMIN); + admin.addScope(Scope.MANAGE_USERS); + admin.addScope(Scope.MANAGE_GROUPS); + admin.addScope(Scope.CHECK_ANALYTICS); + admin.addScope(Scope.CREATE_GROUP); userRepository.save(admin); logger.info("Default super admin created: {}", adminEmail); diff --git a/codehive-backend/src/main/java/com/github/codehive/config/SecurityConfig.java b/codehive-backend/src/main/java/com/github/codehive/config/SecurityConfig.java index a251d45..eea8458 100644 --- a/codehive-backend/src/main/java/com/github/codehive/config/SecurityConfig.java +++ b/codehive-backend/src/main/java/com/github/codehive/config/SecurityConfig.java @@ -50,10 +50,13 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti // Authentication endpoints .requestMatchers(HttpMethod.POST, "/api/auth/login").permitAll() // Password recovery endpoints - .requestMatchers(HttpMethod.POST, "/api/recovery-password/**", "/api/execution/**").permitAll() // WebSocket endpoint + .requestMatchers(HttpMethod.POST, "/api/recovery-password/**").permitAll() + // WebSocket endpoint .requestMatchers("/ws/**").permitAll() // Swagger/OpenAPI documentation .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() + // Assignment upload restricted to teachers + .requestMatchers(HttpMethod.POST, "/api/assignments").hasAnyAuthority("TEACHER", "ADMIN") // All other requests require authentication .anyRequest().authenticated() ) diff --git a/codehive-backend/src/main/java/com/github/codehive/controller/AssignmentController.java b/codehive-backend/src/main/java/com/github/codehive/controller/AssignmentController.java index ce8ffa0..9664983 100644 --- a/codehive-backend/src/main/java/com/github/codehive/controller/AssignmentController.java +++ b/codehive-backend/src/main/java/com/github/codehive/controller/AssignmentController.java @@ -63,7 +63,7 @@ public AssignmentController(AssignmentService assignmentService) { content = @Content(schema = @Schema(implementation = ErrorResponse.class)) ) }) - @PreAuthorize("hasAnyRole('TEACHER', 'ADMIN')") + @PreAuthorize("hasAnyAuthority('TEACHER', 'ADMIN')") @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity> createAssignment( @Valid @RequestPart("metadata") CreateAssignmentRequest metadata, diff --git a/codehive-backend/src/main/resources/application.properties b/codehive-backend/src/main/resources/application.properties index 42e36f7..c7ed008 100644 --- a/codehive-backend/src/main/resources/application.properties +++ b/codehive-backend/src/main/resources/application.properties @@ -42,4 +42,11 @@ spring.rabbitmq.port=${RABBITMQ_PORT:5672} spring.rabbitmq.username=${RABBITMQ_USERNAME:defaultRabbitUser} spring.rabbitmq.password=${RABBITMQ_PASSWORD:defaultRabbitPassword} spring.rabbitmq.queue=${RABBITMQ_QUEUE:defaultQueue} -spring.rabbitmq.result.queue=${RABBITMQ_RESULT_QUEUE:defaultResultQueue} \ No newline at end of file +spring.rabbitmq.result.queue=${RABBITMQ_RESULT_QUEUE:defaultResultQueue} + +# Admin Initializer +app.admin.email=${ADMIN_EMAIL:admin@codehive.com} +app.admin.password=${ADMIN_PASSWORD:Admin@12345} +app.admin.name=${ADMIN_NAME:Super} +app.admin.lastName=${ADMIN_LAST_NAME:Admin} +app.admin.enrollmentNumber=${ADMIN_ENROLLMENT_NUMBER:ADMIN-001} \ No newline at end of file diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/config/RabbitMQConfig.java b/codehive-worker/src/main/java/com/github/codehive/worker/config/RabbitMQConfig.java index b6d5254..a01eb04 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/config/RabbitMQConfig.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/config/RabbitMQConfig.java @@ -3,34 +3,43 @@ import org.springframework.amqp.core.Queue; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RabbitMQConfig { - public static final String QUEUE_NAME = System.getProperty("rabbitmq.queue", "codehive_queue"); - public static final String RESULT_QUEUE_NAME = System.getProperty("rabbitmq.result.queue", "codehive_result_queue"); - public static final String TEST_GENERATION_QUEUE_NAME = System.getProperty("rabbitmq.test-generation.queue", "codehive_test_generation_queue"); - public static final String TEST_GENERATION_RESULT_QUEUE_NAME = System.getProperty("rabbitmq.test-generation.result.queue", "codehive_test_generation_result_queue"); + + @Value("${rabbitmq.queue:codehive_queue}") + private String queueName; + + @Value("${rabbitmq.result.queue:codehive_result_queue}") + private String resultQueueName; + + @Value("${rabbitmq.test-generation.queue:codehive_test_generation_queue}") + private String testGenerationQueueName; + + @Value("${rabbitmq.test-generation.result.queue:codehive_test_generation_result_queue}") + private String testGenerationResultQueueName; @Bean Queue executionQueue() { - return new Queue(QUEUE_NAME, true); + return new Queue(queueName, true); } @Bean Queue resultQueue() { - return new Queue(RESULT_QUEUE_NAME, true); + return new Queue(resultQueueName, true); } @Bean Queue testGenerationQueue() { - return new Queue(TEST_GENERATION_QUEUE_NAME, true); + return new Queue(testGenerationQueueName, true); } @Bean Queue testGenerationResultQueue() { - return new Queue(TEST_GENERATION_RESULT_QUEUE_NAME, true); + return new Queue(testGenerationResultQueueName, true); } @Bean diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/messaging/producer/ExecutionResultProducer.java b/codehive-worker/src/main/java/com/github/codehive/worker/messaging/producer/ExecutionResultProducer.java index e83e4b9..d78ad91 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/messaging/producer/ExecutionResultProducer.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/messaging/producer/ExecutionResultProducer.java @@ -1,16 +1,19 @@ package com.github.codehive.worker.messaging.producer; -import com.github.codehive.worker.config.RabbitMQConfig; import com.github.codehive.worker.model.dto.ExecutionReport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service public class ExecutionResultProducer { private static final Logger logger = LoggerFactory.getLogger(ExecutionResultProducer.class); - + + @Value("${rabbitmq.result.queue:codehive_result_queue}") + private String resultQueueName; + private final RabbitTemplate rabbitTemplate; public ExecutionResultProducer(RabbitTemplate rabbitTemplate) { @@ -18,10 +21,10 @@ public ExecutionResultProducer(RabbitTemplate rabbitTemplate) { } public void sendExecutionResult(ExecutionReport report) { - logger.info("Sending execution result to queue: executionId={}, status={}", + logger.info("Sending execution result to queue: executionId={}, status={}", report.getExecutionId(), report.getOverallStatus()); - - rabbitTemplate.convertAndSend(RabbitMQConfig.RESULT_QUEUE_NAME, report); + + rabbitTemplate.convertAndSend(resultQueueName, report); logger.debug("Execution result sent successfully: executionId={}", report.getExecutionId()); } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/messaging/producer/TestGenerationResultProducer.java b/codehive-worker/src/main/java/com/github/codehive/worker/messaging/producer/TestGenerationResultProducer.java index 239b598..36b0a20 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/messaging/producer/TestGenerationResultProducer.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/messaging/producer/TestGenerationResultProducer.java @@ -3,15 +3,18 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import com.github.codehive.worker.config.RabbitMQConfig; import com.github.codehive.worker.model.dto.queue.TestGenerationResult; @Service public class TestGenerationResultProducer { private static final Logger logger = LoggerFactory.getLogger(TestGenerationResultProducer.class); + @Value("${rabbitmq.test-generation.result.queue:codehive_test_generation_result_queue}") + private String testGenerationResultQueueName; + private final RabbitTemplate rabbitTemplate; public TestGenerationResultProducer(RabbitTemplate rabbitTemplate) { @@ -21,6 +24,6 @@ public TestGenerationResultProducer(RabbitTemplate rabbitTemplate) { public void sendTestGenerationResult(TestGenerationResult result) { logger.info("Sending test generation result - assignmentId={}, success={}, generated={}", result.getAssignmentId(), result.isSuccess(), result.getGeneratedCount()); - rabbitTemplate.convertAndSend(RabbitMQConfig.TEST_GENERATION_RESULT_QUEUE_NAME, result); + rabbitTemplate.convertAndSend(testGenerationResultQueueName, result); } } diff --git a/codehive-worker/src/main/resources/application.properties b/codehive-worker/src/main/resources/application.properties index 106b10b..05228f3 100644 --- a/codehive-worker/src/main/resources/application.properties +++ b/codehive-worker/src/main/resources/application.properties @@ -7,9 +7,11 @@ minio.secretKey=${MINIO_SECRET_KEY:defaultSecretKey} minio.bucketName=${MINIO_BUCKET_NAME:defaultBucketName} # RabbitMQ Configuration -spring.rabbitmq.host=${RABBITMQ_HOST:defaultRabbitHost} +spring.rabbitmq.host=${RABBITMQ_HOST:localhost} spring.rabbitmq.port=${RABBITMQ_PORT:5672} -spring.rabbitmq.username=${RABBITMQ_USERNAME:defaultRabbitUser} -spring.rabbitmq.password=${RABBITMQ_PASSWORD:defaultRabbitPassword} -spring.rabbitmq.queue=${RABBITMQ_QUEUE:defaultQueue} -spring.rabbitmq.result.queue=${RABBITMQ_RESULT_QUEUE:defaultResultQueue} \ No newline at end of file +spring.rabbitmq.username=${RABBITMQ_USERNAME:guest} +spring.rabbitmq.password=${RABBITMQ_PASSWORD:guest} +rabbitmq.queue=${RABBITMQ_QUEUE:codehive_queue} +rabbitmq.result.queue=${RABBITMQ_RESULT_QUEUE:codehive_result_queue} +rabbitmq.test-generation.queue=${RABBITMQ_TEST_GENERATION_QUEUE:codehive_test_generation_queue} +rabbitmq.test-generation.result.queue=${RABBITMQ_TEST_GENERATION_RESULT_QUEUE:codehive_test_generation_result_queue} From 13193e2b8cdd285ddf93a2efce168ea94d2ccb8a Mon Sep 17 00:00:00 2001 From: IrminDev Date: Sat, 9 May 2026 21:54:48 -0600 Subject: [PATCH 18/32] Added security features and added endpoints for execution logic --- AGENTS.md | 3 + .../codehive/controller/AuthController.java | 18 +- .../controller/CheckExecutionController.java | 191 +++++++++------ .../codehive/model/enums/ExecutionStatus.java | 1 + .../github/codehive/service/AuthService.java | 7 + .../service/ExecutionRequestService.java | 25 +- .../codehive/utils/ObjectKeyBuilder.java | 4 + .../worker/model/dto/ExecutionReport.java | 2 + .../worker/model/dto/ExecutionResult.java | 8 + .../worker/model/enums/ExecutionStatus.java | 1 + .../codehive/worker/sandbox/c/CExecutor.java | 225 ++++++++++++----- .../worker/sandbox/cpp/CPPExecutor.java | 224 ++++++++++++----- .../worker/sandbox/java/JavaExecutor.java | 226 +++++++++++++----- .../worker/sandbox/python/PythonExecutor.java | 177 +++++++++----- .../worker/service/TestExecutionService.java | 2 + .../resources/seccomp/sandbox-profile.json | 59 +++++ llms/backend/auth/README.md | 23 +- llms/backend/controller/README.md | 1 + llms/backend/executions/README.md | 7 + llms/backend/model/README.md | 2 +- llms/backend/security/README.md | 15 +- llms/backend/service/README.md | 15 +- llms/worker/OVERVIEW.md | 12 + llms/worker/sandbox/README.md | 90 ++++--- 24 files changed, 964 insertions(+), 374 deletions(-) create mode 100644 codehive-worker/src/main/resources/seccomp/sandbox-profile.json diff --git a/AGENTS.md b/AGENTS.md index bacd269..438d7fe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -171,6 +171,7 @@ All paths are produced by `utils/ObjectKeyBuilder`: | `referenceSolutionSourceCode(assignmentId, ext)` | `test-suites/assignments/{a}/reference/Main.{ext}` | | `executionSourceCode(executionId, ext)` | `test-execution/execution-{e}/source.{ext}` | | `executionTestCaseOutput(executionId)` | `test-execution/execution-{e}/output/` | +| `executionReport(executionId)` | `test-execution/execution-{e}/output/report.json` | | `submissionSourceCode(assignmentId, submissionId, ext)` | `submissions/assignments/{a}/submission-{s}/Main.{ext}` | ## Implemented Features @@ -179,6 +180,8 @@ All paths are produced by `utils/ObjectKeyBuilder`: - Assignment creation: teacher uploads reference solution + test case inputs; worker generates expected outputs asynchronously - Student execution: PRACTICE (inline test cases vs reference solution) and DEFINITIVE (pre-generated outputs) - Execution status polling +- Execution report retrieval: `GET /api/execution/check/{id}/report` fetches per-test-case results from MinIO +- Docker sandbox security hardening: PID limits, capability drop, read-only rootfs, tmpfs mounts, seccomp profile, nobody user, OLE verdict for output floods ## Not Yet Implemented (frontend) - Professor/teacher pages for assignment management diff --git a/codehive-backend/src/main/java/com/github/codehive/controller/AuthController.java b/codehive-backend/src/main/java/com/github/codehive/controller/AuthController.java index 899780e..028f7b9 100644 --- a/codehive-backend/src/main/java/com/github/codehive/controller/AuthController.java +++ b/codehive-backend/src/main/java/com/github/codehive/controller/AuthController.java @@ -7,11 +7,11 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -57,12 +57,8 @@ public AuthController(AuthService authService, CsvRegistrationService csvRegistr content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) @GetMapping("/me") - public ResponseEntity> me(@RequestHeader(value = "Authorization", required = false) String authHeader) { - if (authHeader == null || !authHeader.startsWith("Bearer ") || authHeader.length() <= 7) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - String token = authHeader.substring(7); - UserDTO userDTO = authService.getUserByToken(token); + public ResponseEntity> me(Authentication authentication) { + UserDTO userDTO = authService.getUserByEmail(authentication.getName()); SuccessResponse response = new SuccessResponse<>("User info retrieved", userDTO); return ResponseEntity.ok(response); } @@ -139,13 +135,9 @@ public ResponseEntity>> signupFromCsv( }) @PutMapping("/me/password") public ResponseEntity> updatePassword( - @RequestHeader(value = "Authorization") String authHeader, + Authentication authentication, @Valid @RequestBody UpdatePasswordRequest request) { - if (authHeader == null || !authHeader.startsWith("Bearer ") || authHeader.length() <= 7) { - throw new ValidationException("Invalid Authorization header format"); - } - String token = authHeader.substring(7); - UserDTO userDTO = authService.getUserByToken(token); + UserDTO userDTO = authService.getUserByEmail(authentication.getName()); authService.updatePassword(userDTO.getId(), request); return ResponseEntity.ok(new SuccessResponse<>("Password updated successfully", null)); } diff --git a/codehive-backend/src/main/java/com/github/codehive/controller/CheckExecutionController.java b/codehive-backend/src/main/java/com/github/codehive/controller/CheckExecutionController.java index 0e2c465..9426065 100644 --- a/codehive-backend/src/main/java/com/github/codehive/controller/CheckExecutionController.java +++ b/codehive-backend/src/main/java/com/github/codehive/controller/CheckExecutionController.java @@ -1,23 +1,12 @@ package com.github.codehive.controller; -import java.util.UUID; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - import com.github.codehive.model.dto.ExecutionDTO; +import com.github.codehive.model.dto.queue.ExecutionReport; import com.github.codehive.model.request.execution.ExecutionRequest; import com.github.codehive.model.response.ErrorResponse; import com.github.codehive.model.response.SuccessResponse; import com.github.codehive.ratelimit.RateLimit; import com.github.codehive.service.ExecutionRequestService; - import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -26,9 +15,17 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; - +import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; /** * Controller for code execution endpoints. @@ -36,13 +33,21 @@ */ @RestController @RequestMapping("/api/execution") -@Tag(name = "Code Execution", description = "Code execution and result checking APIs") +@Tag( + name = "Code Execution", + description = "Code execution and result checking APIs" +) public class CheckExecutionController { - private static final Logger logger = LoggerFactory.getLogger(CheckExecutionController.class); - + + private static final Logger logger = LoggerFactory.getLogger( + CheckExecutionController.class + ); + private final ExecutionRequestService executionRequestService; - public CheckExecutionController(ExecutionRequestService executionRequestService) { + public CheckExecutionController( + ExecutionRequestService executionRequestService + ) { this.executionRequestService = executionRequestService; } @@ -50,38 +55,48 @@ public CheckExecutionController(ExecutionRequestService executionRequestService) summary = "Submit code for execution", description = "Submits code to be executed in a sandboxed environment. Returns execution ID for status polling." ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "202", - description = "Execution request accepted", - content = @Content(schema = @Schema(implementation = SuccessResponse.class)) - ), - @ApiResponse( - responseCode = "400", - description = "Validation error", - content = @Content(schema = @Schema(implementation = ErrorResponse.class)) - ), - @ApiResponse( - responseCode = "429", - description = "Too many requests", - content = @Content(schema = @Schema(implementation = ErrorResponse.class)) - ) - }) - @RateLimit(limit = 10, duration = 60, message = "Too many execution requests. Please try again in 1 minute.") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "202", + description = "Execution request accepted", + content = @Content( + schema = @Schema(implementation = SuccessResponse.class) + ) + ), + @ApiResponse( + responseCode = "400", + description = "Validation error", + content = @Content( + schema = @Schema(implementation = ErrorResponse.class) + ) + ), + @ApiResponse( + responseCode = "429", + description = "Too many requests", + content = @Content( + schema = @Schema(implementation = ErrorResponse.class) + ) + ), + } + ) + @RateLimit( + limit = 10, + duration = 60, + message = "Too many execution requests. Please try again in 1 minute." + ) @PostMapping("/check") public ResponseEntity> submitExecution( - @Valid @RequestBody ExecutionRequest request) { - logger.info("[WORKFLOW] POST /api/execution/check - Received execution request: language={}, executionType={}, codeLength={}", - request.getLanguage(), request.getExecutionType(), - request.getCode() != null ? request.getCode().length() : 0); - - ExecutionDTO execution = executionRequestService.requestExecution(request); - - logger.info("[WORKFLOW] POST /api/execution/check - Execution created: executionId={}, status={}", - execution.getId(), execution.getStatus()); - + @Valid @RequestBody ExecutionRequest request + ) { + ExecutionDTO execution = executionRequestService.requestExecution( + request + ); + SuccessResponse response = new SuccessResponse<>( - "Execution request submitted successfully", execution); + "Execution request submitted successfully", + execution + ); return ResponseEntity.status(HttpStatus.ACCEPTED).body(response); } @@ -89,31 +104,73 @@ public ResponseEntity> submitExecution( summary = "Get execution status", description = "Retrieves the current status and results of a code execution by its ID." ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "Execution found", - content = @Content(schema = @Schema(implementation = SuccessResponse.class)) - ), - @ApiResponse( - responseCode = "404", - description = "Execution not found", - content = @Content(schema = @Schema(implementation = ErrorResponse.class)) - ) - }) + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "Execution found", + content = @Content( + schema = @Schema(implementation = SuccessResponse.class) + ) + ), + @ApiResponse( + responseCode = "404", + description = "Execution not found", + content = @Content( + schema = @Schema(implementation = ErrorResponse.class) + ) + ), + } + ) @GetMapping("/check/{id}") public ResponseEntity> getExecution( - @Parameter(description = "Execution ID", required = true) - @PathVariable UUID id) { - logger.info("[WORKFLOW] GET /api/execution/check/{} - Fetching execution status", id); - + @Parameter( + description = "Execution ID", + required = true + ) @PathVariable UUID id + ) { ExecutionDTO execution = executionRequestService.getExecutionById(id); - - logger.info("[WORKFLOW] GET /api/execution/check/{} - Execution found: status={}, timeMs={}, memoryMb={}", - id, execution.getStatus(), execution.getTimeMs(), execution.getMemoryMb()); - SuccessResponse response = new SuccessResponse<>( - "Execution retrieved successfully", execution); + "Execution retrieved successfully", + execution + ); + return ResponseEntity.ok(response); + } + + @Operation( + summary = "Get execution report", + description = "Retrieves the full execution report from object storage, including per-test-case results, timing, memory, and feedback." + ) + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "Report retrieved successfully", + content = @Content( + schema = @Schema(implementation = SuccessResponse.class) + ) + ), + @ApiResponse( + responseCode = "404", + description = "Execution not found or report not available yet", + content = @Content( + schema = @Schema(implementation = ErrorResponse.class) + ) + ), + } + ) + @GetMapping("/check/{id}/report") + public ResponseEntity> getExecutionReport( + @Parameter( + description = "Execution ID", + required = true + ) @PathVariable UUID id + ) { + ExecutionReport report = executionRequestService.getExecutionReport(id); + SuccessResponse response = new SuccessResponse<>( + "Execution report retrieved successfully", + report + ); return ResponseEntity.ok(response); } } diff --git a/codehive-backend/src/main/java/com/github/codehive/model/enums/ExecutionStatus.java b/codehive-backend/src/main/java/com/github/codehive/model/enums/ExecutionStatus.java index 0a4343f..53b4af5 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/enums/ExecutionStatus.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/enums/ExecutionStatus.java @@ -3,6 +3,7 @@ public enum ExecutionStatus { TLE, // Time Limit Exceeded MLE, // Memory Limit Exceeded + OLE, // Output Limit Exceeded RTE, // Runtime Error CE, // Compilation Error WA, // Wrong Answer diff --git a/codehive-backend/src/main/java/com/github/codehive/service/AuthService.java b/codehive-backend/src/main/java/com/github/codehive/service/AuthService.java index df079c3..5bfeb8b 100644 --- a/codehive-backend/src/main/java/com/github/codehive/service/AuthService.java +++ b/codehive-backend/src/main/java/com/github/codehive/service/AuthService.java @@ -223,6 +223,13 @@ public UserDTO getUserByToken(String token) { return UserMapper.toDTO(user); } + @Transactional(readOnly = true) + public UserDTO getUserByEmail(String email) { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new IncorrectCredentialsException("User not found")); + return UserMapper.toDTO(user); + } + private String generateToken(User user) { Map claims = new HashMap<>(); claims.put("userId", user.getId()); diff --git a/codehive-backend/src/main/java/com/github/codehive/service/ExecutionRequestService.java b/codehive-backend/src/main/java/com/github/codehive/service/ExecutionRequestService.java index d020ec9..2723b20 100644 --- a/codehive-backend/src/main/java/com/github/codehive/service/ExecutionRequestService.java +++ b/codehive-backend/src/main/java/com/github/codehive/service/ExecutionRequestService.java @@ -1,14 +1,17 @@ package com.github.codehive.service; +import java.io.InputStream; import java.util.List; import java.util.UUID; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.fasterxml.jackson.databind.ObjectMapper; import com.github.codehive.messaging.producer.ExecutionRequestProducer; import com.github.codehive.model.dto.ExecutionDTO; import com.github.codehive.model.dto.queue.ExecutionJob; +import com.github.codehive.model.dto.queue.ExecutionReport; import com.github.codehive.model.entity.Assignment; import com.github.codehive.model.entity.Execution; import com.github.codehive.model.entity.ReferenceSolution; @@ -35,6 +38,7 @@ public class ExecutionRequestService { private final AssignmentRepository assignmentRepository; private final ReferenceSolutionRepository referenceSolutionRepository; private final TestCaseRepository testCaseRepository; + private final ObjectMapper objectMapper; public ExecutionRequestService(ExecutionRequestProducer executionRequestProducer, ExecutionRepository executionRepository, @@ -42,7 +46,8 @@ public ExecutionRequestService(ExecutionRequestProducer executionRequestProducer UserRepository userRepository, AssignmentRepository assignmentRepository, ReferenceSolutionRepository referenceSolutionRepository, - TestCaseRepository testCaseRepository) { + TestCaseRepository testCaseRepository, + ObjectMapper objectMapper) { this.executionRequestProducer = executionRequestProducer; this.executionRepository = executionRepository; this.objectStorageService = objectStorageService; @@ -50,6 +55,7 @@ public ExecutionRequestService(ExecutionRequestProducer executionRequestProducer this.assignmentRepository = assignmentRepository; this.referenceSolutionRepository = referenceSolutionRepository; this.testCaseRepository = testCaseRepository; + this.objectMapper = objectMapper; } @Transactional @@ -91,6 +97,23 @@ public ExecutionDTO getExecutionById(UUID id) { return ExecutionMapper.toDTO(execution); } + @Transactional(readOnly = true) + public ExecutionReport getExecutionReport(UUID id) { + Execution execution = executionRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("Execution not found with id: " + id)); + + String reportKey = ObjectKeyBuilder.executionReport(id); + try { + InputStream reportStream = objectStorageService.download(reportKey); + return objectMapper.readValue(reportStream, ExecutionReport.class); + } catch (Exception e) { + if (execution.getStatus() != null && execution.getStatus().name().equals("PENDING")) { + throw new EntityNotFoundException("Report not available yet: execution " + id + " is still pending"); + } + throw new EntityNotFoundException("Report not found for execution: " + id); + } + } + private ExecutionJob buildExecutionJob(Execution execution, ExecutionRequest request, Assignment assignment, String extension) { String sourceKey = ObjectKeyBuilder.executionSourceCode(execution.getId(), extension); diff --git a/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java b/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java index 86a39e1..7b5bbdb 100644 --- a/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java +++ b/codehive-backend/src/main/java/com/github/codehive/utils/ObjectKeyBuilder.java @@ -34,4 +34,8 @@ public static String executionTestCaseOutput(UUID executionId) { public static String executionSourceCode(UUID executionId, String fileExtension) { return String.format("test-execution/execution-%s/source.%s", executionId, fileExtension); } + + public static String executionReport(UUID executionId) { + return String.format("test-execution/execution-%s/output/report.json", executionId); + } } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionReport.java b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionReport.java index 86a235a..8bbaf3d 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionReport.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionReport.java @@ -86,6 +86,8 @@ public void determineOverallStatus() { this.overallStatus = ExecutionStatus.TLE; } else if (testCaseResults.stream().anyMatch(r -> r.getStatus() == ExecutionStatus.MLE)) { this.overallStatus = ExecutionStatus.MLE; + } else if (testCaseResults.stream().anyMatch(r -> r.getStatus() == ExecutionStatus.OLE)) { + this.overallStatus = ExecutionStatus.OLE; } else if (testCaseResults.stream().anyMatch(r -> r.getStatus() == ExecutionStatus.RTE)) { this.overallStatus = ExecutionStatus.RTE; } else { diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionResult.java b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionResult.java index 3277110..0347938 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionResult.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/model/dto/ExecutionResult.java @@ -55,6 +55,14 @@ public static ExecutionResult memoryLimitExceeded(Long memoryUsed) { return result; } + public static ExecutionResult outputLimitExceeded(String truncatedOutput, Long executionTimeMs) { + ExecutionResult result = new ExecutionResult(); + result.status = ExecutionStatus.OLE; + result.output = truncatedOutput; + result.executionTimeMs = executionTimeMs; + return result; + } + public static ExecutionResult success(String output, Long executionTime, Long memoryUsed) { ExecutionResult result = new ExecutionResult(); result.status = ExecutionStatus.AC; diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/model/enums/ExecutionStatus.java b/codehive-worker/src/main/java/com/github/codehive/worker/model/enums/ExecutionStatus.java index dd367c4..ba9f2ab 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/model/enums/ExecutionStatus.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/model/enums/ExecutionStatus.java @@ -3,6 +3,7 @@ public enum ExecutionStatus { TLE, // Time Limit Exceeded MLE, // Memory Limit Exceeded + OLE, // Output Limit Exceeded RTE, // Runtime Error CE, // Compilation Error WA, // Wrong Answer diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/c/CExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/c/CExecutor.java index 3290639..deec4c0 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/c/CExecutor.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/c/CExecutor.java @@ -7,19 +7,31 @@ import com.github.dockerjava.api.command.CreateContainerResponse; import com.github.dockerjava.api.command.WaitContainerResultCallback; import com.github.dockerjava.api.model.Bind; +import com.github.dockerjava.api.model.Capability; +import com.github.dockerjava.api.model.Frame; import com.github.dockerjava.api.model.HostConfig; +import com.github.dockerjava.api.model.Mount; +import com.github.dockerjava.api.model.MountType; import com.github.dockerjava.api.model.Statistics; +import com.github.dockerjava.api.model.TmpfsOptions; +import com.github.dockerjava.api.model.Ulimit; import com.github.dockerjava.api.model.Volume; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.*; @Component("C") @@ -29,6 +41,20 @@ public class CExecutor implements LanguageExecutor { private static final String GCC_IMAGE = "gcc:latest"; private static final long DEFAULT_TIME_LIMIT_MS = 5000L; private static final long DEFAULT_MEMORY_LIMIT_MB = 256L; + private static final int OUTPUT_LIMIT_BYTES = 4 * 1024 * 1024; + private static final int COMPILE_STDERR_LIMIT_BYTES = 256 * 1024; + private static final long PIDS_LIMIT = 32L; + + private static final String SECCOMP_PROFILE; + static { + String profile = null; + try (InputStream is = CExecutor.class.getResourceAsStream("/seccomp/sandbox-profile.json")) { + if (is != null) profile = new String(is.readAllBytes(), StandardCharsets.UTF_8); + } catch (Exception e) { + // Runs without seccomp if profile fails to load + } + SECCOMP_PROFILE = profile; + } public CExecutor(DockerClient dockerClient) { this.dockerClient = dockerClient; @@ -61,25 +87,26 @@ public ExecutionResult execute(InputStream sourceCode, InputStream testInput, Lo Path inputFile = tempDir.resolve("input.txt"); try { - // Save source code - Files.copy(sourceCode, sourceFile, StandardCopyOption.REPLACE_EXISTING); + Files.setPosixFilePermissions(tempDir, PosixFilePermissions.fromString("rwxrwxrwx")); + + byte[] sourceBytes = sourceCode.readNBytes(512 * 1024); + Files.write(sourceFile, sourceBytes); + Files.setPosixFilePermissions(sourceFile, PosixFilePermissions.fromString("r--r--r--")); - // Save test input if provided if (testInput != null) { - Files.copy(testInput, inputFile, StandardCopyOption.REPLACE_EXISTING); + byte[] inputBytes = testInput.readNBytes(64 * 1024 * 1024); + Files.write(inputFile, inputBytes); + Files.setPosixFilePermissions(inputFile, PosixFilePermissions.fromString("r--r--r--")); } - // Compile ExecutionResult compileResult = compile(tempDir); if (compileResult != null) { - return compileResult; // Compilation error + return compileResult; } - // Execute return executeCode(tempDir, inputFile, timeLimitMs, memoryLimitMb); } finally { - // Cleanup temp directory deleteDirectory(tempDir); } } @@ -87,32 +114,42 @@ public ExecutionResult execute(InputStream sourceCode, InputStream testInput, Lo private ExecutionResult compile(Path workDir) { String containerId = null; try { - // Create container for compilation HostConfig hostConfig = HostConfig.newHostConfig() .withBinds(new Bind(workDir.toString(), new Volume("/workspace"))) - .withMemory(512 * 1024 * 1024L) // 512MB for compilation - .withNetworkMode("none"); + .withMemory(512 * 1024 * 1024L) + .withNetworkMode("none") + .withPidsLimit(PIDS_LIMIT) + .withCapDrop(Capability.ALL) + .withReadonlyRootfs(true) + .withMounts(List.of( + new Mount().withType(MountType.TMPFS).withTarget("/tmp") + .withTmpfsOptions(new TmpfsOptions().withSizeBytes(64L * 1024 * 1024).withMode(01777)), + new Mount().withType(MountType.TMPFS).withTarget("/run") + .withTmpfsOptions(new TmpfsOptions().withSizeBytes(8L * 1024 * 1024).withMode(0755)) + )) + .withUlimits(new Ulimit[]{new Ulimit("fsize", 67108864L, 67108864L)}) + .withSecurityOpts(buildSecurityOpts()); CreateContainerResponse container = dockerClient.createContainerCmd(GCC_IMAGE) .withHostConfig(hostConfig) .withWorkingDir("/workspace") - .withCmd("gcc", "-o", "program", "main.c", "-lm") // -lm for math library + .withUser("nobody") + .withCmd("gcc", "-o", "program", "main.c", "-lm") .exec(); containerId = container.getId(); dockerClient.startContainerCmd(containerId).exec(); - // Wait for compilation int exitCode = dockerClient.waitContainerCmd(containerId) .exec(new WaitContainerResultCallback()) .awaitStatusCode(30, TimeUnit.SECONDS); if (exitCode != 0) { - String stderr = getContainerLogs(containerId, true); + String stderr = getContainerLogsCompile(containerId); return ExecutionResult.compilationError(stderr); } - return null; // Success + return null; } catch (Exception e) { logger.error("Compilation error", e); return ExecutionResult.compilationError("Compilation failed: " + e.getMessage()); @@ -135,9 +172,20 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit HostConfig hostConfig = HostConfig.newHostConfig() .withBinds(new Bind(workDir.toString(), new Volume("/workspace"))) .withMemory(memoryLimitMb * 1024 * 1024L) - .withMemorySwap(memoryLimitMb * 1024 * 1024L) // Disable swap - .withCpuQuota(100000L) // 1 CPU - .withNetworkMode("none"); + .withMemorySwap(memoryLimitMb * 1024 * 1024L) + .withCpuQuota(100000L) + .withNetworkMode("none") + .withPidsLimit(PIDS_LIMIT) + .withCapDrop(Capability.ALL) + .withReadonlyRootfs(true) + .withMounts(List.of( + new Mount().withType(MountType.TMPFS).withTarget("/tmp") + .withTmpfsOptions(new TmpfsOptions().withSizeBytes(32L * 1024 * 1024).withMode(01777)), + new Mount().withType(MountType.TMPFS).withTarget("/run") + .withTmpfsOptions(new TmpfsOptions().withSizeBytes(8L * 1024 * 1024).withMode(0755)) + )) + .withUlimits(new Ulimit[]{new Ulimit("fsize", 33554432L, 33554432L)}) + .withSecurityOpts(buildSecurityOpts()); String[] cmd; if (Files.exists(inputFile) && Files.size(inputFile) > 0) { @@ -149,6 +197,7 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit CreateContainerResponse container = dockerClient.createContainerCmd(GCC_IMAGE) .withHostConfig(hostConfig) .withWorkingDir("/workspace") + .withUser("nobody") .withCmd(cmd) .exec(); @@ -156,9 +205,8 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit final String finalContainerId = containerId; dockerClient.startContainerCmd(containerId).exec(); - // Wait with timeout ExecutorService executor = Executors.newSingleThreadExecutor(); - Future future = executor.submit(() -> + Future future = executor.submit(() -> dockerClient.waitContainerCmd(finalContainerId) .exec(new WaitContainerResultCallback()) .awaitStatusCode() @@ -177,20 +225,25 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit long executionTime = System.currentTimeMillis() - startTime; - // Check for memory limit (Docker kills container if exceeded) - if (exitCode == 137) { // SIGKILL - often means OOM + if (exitCode == 137) { return ExecutionResult.memoryLimitExceeded(memoryLimitMb * 1024L); } - String stdout = getContainerLogs(containerId, false); - String stderr = getContainerLogs(containerId, true); + int[] sharedBytes = {0}; + LogResult stdoutResult = getContainerLogsLimited(containerId, false, sharedBytes); + LogResult stderrResult = getContainerLogsLimited(containerId, true, sharedBytes); + + if (stdoutResult.truncated() || stderrResult.truncated()) { + return ExecutionResult.outputLimitExceeded( + stdoutResult.content() + "\n[Output truncated: exceeded 4 MB limit]", executionTime); + } if (exitCode != 0) { - return ExecutionResult.runtimeError(stderr, exitCode, executionTime); + return ExecutionResult.runtimeError(stderrResult.content(), exitCode, executionTime); } - Long memoryUsed = getContainerPeakMemory(containerId); - return ExecutionResult.success(stdout, executionTime, memoryUsed); + Long memoryUsed = getContainerPeakMemory(containerId); + return ExecutionResult.success(stdoutResult.content(), executionTime, memoryUsed); } catch (Exception e) { logger.error("Execution error", e); @@ -206,44 +259,91 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit } } - private String getContainerLogs(String containerId, boolean stderr) { + private record LogResult(String content, boolean truncated) {} + + private LogResult getContainerLogsLimited(String containerId, boolean useStderr, int[] sharedBytes) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + boolean[] truncated = {false}; try { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); dockerClient.logContainerCmd(containerId) - .withStdOut(!stderr) - .withStdErr(stderr) - .exec(new com.github.dockerjava.api.async.ResultCallback.Adapter() { - @Override - public void onNext(com.github.dockerjava.api.model.Frame frame) { - try { - outputStream.write(frame.getPayload()); - } catch (Exception e) { - logger.error("Error reading frame", e); - } + .withStdOut(!useStderr) + .withStdErr(useStderr) + .exec(new ResultCallback.Adapter() { + @Override + public void onNext(Frame frame) { + byte[] payload = frame.getPayload(); + if (payload == null) return; + synchronized (sharedBytes) { + int remaining = OUTPUT_LIMIT_BYTES - sharedBytes[0]; + if (remaining <= 0) { truncated[0] = true; return; } + int toWrite = Math.min(payload.length, remaining); + try { baos.write(payload, 0, toWrite); } catch (Exception ignored) {} + sharedBytes[0] += toWrite; + if (toWrite < payload.length) truncated[0] = true; } - }) - .awaitCompletion(5, TimeUnit.SECONDS); - - return outputStream.toString(StandardCharsets.UTF_8); + } + }).awaitCompletion(5, TimeUnit.SECONDS); } catch (Exception e) { logger.error("Failed to get container logs", e); - return ""; } + return new LogResult(baos.toString(StandardCharsets.UTF_8), truncated[0]); + } + + private String getContainerLogsCompile(String containerId) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int[] bytesWritten = {0}; + try { + dockerClient.logContainerCmd(containerId) + .withStdOut(false) + .withStdErr(true) + .exec(new ResultCallback.Adapter() { + @Override + public void onNext(Frame frame) { + byte[] payload = frame.getPayload(); + if (payload == null) return; + int remaining = COMPILE_STDERR_LIMIT_BYTES - bytesWritten[0]; + if (remaining <= 0) return; + int toWrite = Math.min(payload.length, remaining); + try { baos.write(payload, 0, toWrite); } catch (Exception ignored) {} + bytesWritten[0] += toWrite; + } + }).awaitCompletion(5, TimeUnit.SECONDS); + } catch (Exception e) { + logger.error("Failed to get compile logs", e); + } + return baos.toString(StandardCharsets.UTF_8); + } + + private List buildSecurityOpts() { + List opts = new ArrayList<>(); + opts.add("no-new-privileges:true"); + if (SECCOMP_PROFILE != null) { + opts.add("seccomp=" + SECCOMP_PROFILE); + } + return opts; } private void deleteDirectory(Path directory) { try { - Files.walk(directory) - .sorted((a, b) -> -a.compareTo(b)) - .forEach(path -> { - try { - Files.deleteIfExists(path); - } catch (Exception e) { - logger.warn("Failed to delete: " + path, e); - } - }); + Path realBase = directory.toRealPath(); + Files.walkFileTree(directory, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + try { + if (file.toRealPath().startsWith(realBase)) Files.deleteIfExists(file); + } catch (Exception e) { + logger.warn("Failed to delete: {}", file, e); + } + return FileVisitResult.CONTINUE; + } + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) { + try { Files.deleteIfExists(dir); } catch (Exception e) { logger.warn("Failed to delete dir: {}", dir, e); } + return FileVisitResult.CONTINUE; + } + }); } catch (Exception e) { - logger.warn("Failed to delete directory: " + directory, e); + logger.warn("Failed to delete directory: {}", directory, e); } } @@ -251,39 +351,36 @@ private Long getContainerPeakMemory(String containerId) { try { final CountDownLatch latch = new CountDownLatch(1); final Statistics[] statsHolder = new Statistics[1]; - + dockerClient.statsCmd(containerId) - .withNoStream(true) // Single snapshot + .withNoStream(true) .exec(new ResultCallback.Adapter() { @Override public void onNext(Statistics stats) { statsHolder[0] = stats; } - @Override public void onComplete() { latch.countDown(); super.onComplete(); } - @Override public void onError(Throwable throwable) { latch.countDown(); super.onError(throwable); } }); - - // Wait for completion (with timeout) + latch.await(5, TimeUnit.SECONDS); Statistics stats = statsHolder[0]; - + if (stats != null && stats.getMemoryStats() != null) { Long maxUsage = stats.getMemoryStats().getMaxUsage(); - return maxUsage != null ? maxUsage / (1024 * 1024) : 0L; // Bytes to MB + return maxUsage != null ? maxUsage / (1024 * 1024) : 0L; } } catch (Exception e) { logger.warn("Failed to get memory stats for container {}", containerId, e); } - return 0L; // Fallback + return 0L; } } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/cpp/CPPExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/cpp/CPPExecutor.java index 5e68446..7ceed24 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/cpp/CPPExecutor.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/cpp/CPPExecutor.java @@ -7,19 +7,31 @@ import com.github.dockerjava.api.command.CreateContainerResponse; import com.github.dockerjava.api.command.WaitContainerResultCallback; import com.github.dockerjava.api.model.Bind; +import com.github.dockerjava.api.model.Capability; +import com.github.dockerjava.api.model.Frame; import com.github.dockerjava.api.model.HostConfig; +import com.github.dockerjava.api.model.Mount; +import com.github.dockerjava.api.model.MountType; import com.github.dockerjava.api.model.Statistics; +import com.github.dockerjava.api.model.TmpfsOptions; +import com.github.dockerjava.api.model.Ulimit; import com.github.dockerjava.api.model.Volume; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.*; @Component("CPP") @@ -29,6 +41,20 @@ public class CPPExecutor implements LanguageExecutor { private static final String GCC_IMAGE = "gcc:latest"; private static final long DEFAULT_TIME_LIMIT_MS = 5000L; private static final long DEFAULT_MEMORY_LIMIT_MB = 256L; + private static final int OUTPUT_LIMIT_BYTES = 4 * 1024 * 1024; + private static final int COMPILE_STDERR_LIMIT_BYTES = 256 * 1024; + private static final long PIDS_LIMIT = 32L; + + private static final String SECCOMP_PROFILE; + static { + String profile = null; + try (InputStream is = CPPExecutor.class.getResourceAsStream("/seccomp/sandbox-profile.json")) { + if (is != null) profile = new String(is.readAllBytes(), StandardCharsets.UTF_8); + } catch (Exception e) { + // Runs without seccomp if profile fails to load + } + SECCOMP_PROFILE = profile; + } public CPPExecutor(DockerClient dockerClient) { this.dockerClient = dockerClient; @@ -61,25 +87,26 @@ public ExecutionResult execute(InputStream sourceCode, InputStream testInput, Lo Path inputFile = tempDir.resolve("input.txt"); try { - // Save source code - Files.copy(sourceCode, sourceFile, StandardCopyOption.REPLACE_EXISTING); + Files.setPosixFilePermissions(tempDir, PosixFilePermissions.fromString("rwxrwxrwx")); + + byte[] sourceBytes = sourceCode.readNBytes(512 * 1024); + Files.write(sourceFile, sourceBytes); + Files.setPosixFilePermissions(sourceFile, PosixFilePermissions.fromString("r--r--r--")); - // Save test input if provided if (testInput != null) { - Files.copy(testInput, inputFile, StandardCopyOption.REPLACE_EXISTING); + byte[] inputBytes = testInput.readNBytes(64 * 1024 * 1024); + Files.write(inputFile, inputBytes); + Files.setPosixFilePermissions(inputFile, PosixFilePermissions.fromString("r--r--r--")); } - // Compile ExecutionResult compileResult = compile(tempDir); if (compileResult != null) { - return compileResult; // Compilation error + return compileResult; } - // Execute return executeCode(tempDir, inputFile, timeLimitMs, memoryLimitMb); } finally { - // Cleanup temp directory deleteDirectory(tempDir); } } @@ -87,32 +114,42 @@ public ExecutionResult execute(InputStream sourceCode, InputStream testInput, Lo private ExecutionResult compile(Path workDir) { String containerId = null; try { - // Create container for compilation HostConfig hostConfig = HostConfig.newHostConfig() .withBinds(new Bind(workDir.toString(), new Volume("/workspace"))) - .withMemory(512 * 1024 * 1024L) // 512MB for compilation - .withNetworkMode("none"); + .withMemory(512 * 1024 * 1024L) + .withNetworkMode("none") + .withPidsLimit(PIDS_LIMIT) + .withCapDrop(Capability.ALL) + .withReadonlyRootfs(true) + .withMounts(List.of( + new Mount().withType(MountType.TMPFS).withTarget("/tmp") + .withTmpfsOptions(new TmpfsOptions().withSizeBytes(64L * 1024 * 1024).withMode(01777)), + new Mount().withType(MountType.TMPFS).withTarget("/run") + .withTmpfsOptions(new TmpfsOptions().withSizeBytes(8L * 1024 * 1024).withMode(0755)) + )) + .withUlimits(new Ulimit[]{new Ulimit("fsize", 67108864L, 67108864L)}) + .withSecurityOpts(buildSecurityOpts()); CreateContainerResponse container = dockerClient.createContainerCmd(GCC_IMAGE) .withHostConfig(hostConfig) .withWorkingDir("/workspace") - .withCmd("g++", "-o", "program", "main.cpp", "-std=c++17", "-lm") // C++17 standard + .withUser("nobody") + .withCmd("g++", "-o", "program", "main.cpp", "-std=c++17", "-lm") .exec(); containerId = container.getId(); dockerClient.startContainerCmd(containerId).exec(); - // Wait for compilation int exitCode = dockerClient.waitContainerCmd(containerId) .exec(new WaitContainerResultCallback()) .awaitStatusCode(30, TimeUnit.SECONDS); if (exitCode != 0) { - String stderr = getContainerLogs(containerId, true); + String stderr = getContainerLogsCompile(containerId); return ExecutionResult.compilationError(stderr); } - return null; // Success + return null; } catch (Exception e) { logger.error("Compilation error", e); return ExecutionResult.compilationError("Compilation failed: " + e.getMessage()); @@ -135,9 +172,20 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit HostConfig hostConfig = HostConfig.newHostConfig() .withBinds(new Bind(workDir.toString(), new Volume("/workspace"))) .withMemory(memoryLimitMb * 1024 * 1024L) - .withMemorySwap(memoryLimitMb * 1024 * 1024L) // Disable swap - .withCpuQuota(100000L) // 1 CPU - .withNetworkMode("none"); + .withMemorySwap(memoryLimitMb * 1024 * 1024L) + .withCpuQuota(100000L) + .withNetworkMode("none") + .withPidsLimit(PIDS_LIMIT) + .withCapDrop(Capability.ALL) + .withReadonlyRootfs(true) + .withMounts(List.of( + new Mount().withType(MountType.TMPFS).withTarget("/tmp") + .withTmpfsOptions(new TmpfsOptions().withSizeBytes(32L * 1024 * 1024).withMode(01777)), + new Mount().withType(MountType.TMPFS).withTarget("/run") + .withTmpfsOptions(new TmpfsOptions().withSizeBytes(8L * 1024 * 1024).withMode(0755)) + )) + .withUlimits(new Ulimit[]{new Ulimit("fsize", 33554432L, 33554432L)}) + .withSecurityOpts(buildSecurityOpts()); String[] cmd; if (Files.exists(inputFile) && Files.size(inputFile) > 0) { @@ -149,6 +197,7 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit CreateContainerResponse container = dockerClient.createContainerCmd(GCC_IMAGE) .withHostConfig(hostConfig) .withWorkingDir("/workspace") + .withUser("nobody") .withCmd(cmd) .exec(); @@ -156,9 +205,8 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit final String finalContainerId = containerId; dockerClient.startContainerCmd(containerId).exec(); - // Wait with timeout ExecutorService executor = Executors.newSingleThreadExecutor(); - Future future = executor.submit(() -> + Future future = executor.submit(() -> dockerClient.waitContainerCmd(finalContainerId) .exec(new WaitContainerResultCallback()) .awaitStatusCode() @@ -177,21 +225,25 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit long executionTime = System.currentTimeMillis() - startTime; - // Check for memory limit (Docker kills container if exceeded) - if (exitCode == 137) { // SIGKILL - often means OOM + if (exitCode == 137) { return ExecutionResult.memoryLimitExceeded(memoryLimitMb * 1024L); } - String stdout = getContainerLogs(containerId, false); - String stderr = getContainerLogs(containerId, true); + int[] sharedBytes = {0}; + LogResult stdoutResult = getContainerLogsLimited(containerId, false, sharedBytes); + LogResult stderrResult = getContainerLogsLimited(containerId, true, sharedBytes); + + if (stdoutResult.truncated() || stderrResult.truncated()) { + return ExecutionResult.outputLimitExceeded( + stdoutResult.content() + "\n[Output truncated: exceeded 4 MB limit]", executionTime); + } if (exitCode != 0) { - return ExecutionResult.runtimeError(stderr, exitCode, executionTime); + return ExecutionResult.runtimeError(stderrResult.content(), exitCode, executionTime); } Long memoryUsed = getContainerPeakMemory(containerId); - - return ExecutionResult.success(stdout, executionTime, memoryUsed); + return ExecutionResult.success(stdoutResult.content(), executionTime, memoryUsed); } catch (Exception e) { logger.error("Execution error", e); @@ -207,44 +259,91 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit } } - private String getContainerLogs(String containerId, boolean stderr) { + private record LogResult(String content, boolean truncated) {} + + private LogResult getContainerLogsLimited(String containerId, boolean useStderr, int[] sharedBytes) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + boolean[] truncated = {false}; try { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); dockerClient.logContainerCmd(containerId) - .withStdOut(!stderr) - .withStdErr(stderr) - .exec(new com.github.dockerjava.api.async.ResultCallback.Adapter() { - @Override - public void onNext(com.github.dockerjava.api.model.Frame frame) { - try { - outputStream.write(frame.getPayload()); - } catch (Exception e) { - logger.error("Error reading frame", e); - } + .withStdOut(!useStderr) + .withStdErr(useStderr) + .exec(new ResultCallback.Adapter() { + @Override + public void onNext(Frame frame) { + byte[] payload = frame.getPayload(); + if (payload == null) return; + synchronized (sharedBytes) { + int remaining = OUTPUT_LIMIT_BYTES - sharedBytes[0]; + if (remaining <= 0) { truncated[0] = true; return; } + int toWrite = Math.min(payload.length, remaining); + try { baos.write(payload, 0, toWrite); } catch (Exception ignored) {} + sharedBytes[0] += toWrite; + if (toWrite < payload.length) truncated[0] = true; } - }) - .awaitCompletion(5, TimeUnit.SECONDS); - - return outputStream.toString(StandardCharsets.UTF_8); + } + }).awaitCompletion(5, TimeUnit.SECONDS); } catch (Exception e) { logger.error("Failed to get container logs", e); - return ""; } + return new LogResult(baos.toString(StandardCharsets.UTF_8), truncated[0]); + } + + private String getContainerLogsCompile(String containerId) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int[] bytesWritten = {0}; + try { + dockerClient.logContainerCmd(containerId) + .withStdOut(false) + .withStdErr(true) + .exec(new ResultCallback.Adapter() { + @Override + public void onNext(Frame frame) { + byte[] payload = frame.getPayload(); + if (payload == null) return; + int remaining = COMPILE_STDERR_LIMIT_BYTES - bytesWritten[0]; + if (remaining <= 0) return; + int toWrite = Math.min(payload.length, remaining); + try { baos.write(payload, 0, toWrite); } catch (Exception ignored) {} + bytesWritten[0] += toWrite; + } + }).awaitCompletion(5, TimeUnit.SECONDS); + } catch (Exception e) { + logger.error("Failed to get compile logs", e); + } + return baos.toString(StandardCharsets.UTF_8); + } + + private List buildSecurityOpts() { + List opts = new ArrayList<>(); + opts.add("no-new-privileges:true"); + if (SECCOMP_PROFILE != null) { + opts.add("seccomp=" + SECCOMP_PROFILE); + } + return opts; } private void deleteDirectory(Path directory) { try { - Files.walk(directory) - .sorted((a, b) -> -a.compareTo(b)) - .forEach(path -> { - try { - Files.deleteIfExists(path); - } catch (Exception e) { - logger.warn("Failed to delete: " + path, e); - } - }); + Path realBase = directory.toRealPath(); + Files.walkFileTree(directory, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + try { + if (file.toRealPath().startsWith(realBase)) Files.deleteIfExists(file); + } catch (Exception e) { + logger.warn("Failed to delete: {}", file, e); + } + return FileVisitResult.CONTINUE; + } + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) { + try { Files.deleteIfExists(dir); } catch (Exception e) { logger.warn("Failed to delete dir: {}", dir, e); } + return FileVisitResult.CONTINUE; + } + }); } catch (Exception e) { - logger.warn("Failed to delete directory: " + directory, e); + logger.warn("Failed to delete directory: {}", directory, e); } } @@ -252,39 +351,36 @@ private Long getContainerPeakMemory(String containerId) { try { final CountDownLatch latch = new CountDownLatch(1); final Statistics[] statsHolder = new Statistics[1]; - + dockerClient.statsCmd(containerId) - .withNoStream(true) // Single snapshot + .withNoStream(true) .exec(new ResultCallback.Adapter() { @Override public void onNext(Statistics stats) { statsHolder[0] = stats; } - @Override public void onComplete() { latch.countDown(); super.onComplete(); } - @Override public void onError(Throwable throwable) { latch.countDown(); super.onError(throwable); } }); - - // Wait for completion (with timeout) + latch.await(5, TimeUnit.SECONDS); Statistics stats = statsHolder[0]; - + if (stats != null && stats.getMemoryStats() != null) { Long maxUsage = stats.getMemoryStats().getMaxUsage(); - return maxUsage != null ? maxUsage / (1024 * 1024) : 0L; // Bytes to MB + return maxUsage != null ? maxUsage / (1024 * 1024) : 0L; } } catch (Exception e) { logger.warn("Failed to get memory stats for container {}", containerId, e); } - return 0L; // Fallback + return 0L; } } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java index 4558d67..9d9338f 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/java/JavaExecutor.java @@ -4,24 +4,34 @@ import com.github.codehive.worker.sandbox.LanguageExecutor; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.async.ResultCallback; -import com.github.dockerjava.api.async.ResultCallback.Adapter; import com.github.dockerjava.api.command.CreateContainerResponse; import com.github.dockerjava.api.command.WaitContainerResultCallback; import com.github.dockerjava.api.model.Bind; +import com.github.dockerjava.api.model.Capability; import com.github.dockerjava.api.model.Frame; import com.github.dockerjava.api.model.HostConfig; +import com.github.dockerjava.api.model.Mount; +import com.github.dockerjava.api.model.MountType; import com.github.dockerjava.api.model.Statistics; +import com.github.dockerjava.api.model.TmpfsOptions; +import com.github.dockerjava.api.model.Ulimit; import com.github.dockerjava.api.model.Volume; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.*; @Component("JAVA") @@ -31,6 +41,20 @@ public class JavaExecutor implements LanguageExecutor { private static final String JAVA_IMAGE = "eclipse-temurin:21-jdk-ubi10-minimal"; private static final long DEFAULT_TIME_LIMIT_MS = 5000L; private static final long DEFAULT_MEMORY_LIMIT_MB = 256L; + private static final int OUTPUT_LIMIT_BYTES = 4 * 1024 * 1024; + private static final int COMPILE_STDERR_LIMIT_BYTES = 256 * 1024; + private static final long PIDS_LIMIT = 64L; + + private static final String SECCOMP_PROFILE; + static { + String profile = null; + try (InputStream is = JavaExecutor.class.getResourceAsStream("/seccomp/sandbox-profile.json")) { + if (is != null) profile = new String(is.readAllBytes(), StandardCharsets.UTF_8); + } catch (Exception e) { + // Runs without seccomp if profile fails to load + } + SECCOMP_PROFILE = profile; + } public JavaExecutor(DockerClient dockerClient) { this.dockerClient = dockerClient; @@ -63,25 +87,28 @@ public ExecutionResult execute(InputStream sourceCode, InputStream testInput, Lo Path inputFile = tempDir.resolve("input.txt"); try { - // Save source code - Files.copy(sourceCode, sourceFile, StandardCopyOption.REPLACE_EXISTING); + // World-accessible workspace so container's nobody user can read/write + Files.setPosixFilePermissions(tempDir, PosixFilePermissions.fromString("rwxrwxrwx")); + + // Bounded source copy (512 KB max) + byte[] sourceBytes = sourceCode.readNBytes(512 * 1024); + Files.write(sourceFile, sourceBytes); + Files.setPosixFilePermissions(sourceFile, PosixFilePermissions.fromString("r--r--r--")); - // Save test input if provided if (testInput != null) { - Files.copy(testInput, inputFile, StandardCopyOption.REPLACE_EXISTING); + byte[] inputBytes = testInput.readNBytes(64 * 1024 * 1024); + Files.write(inputFile, inputBytes); + Files.setPosixFilePermissions(inputFile, PosixFilePermissions.fromString("r--r--r--")); } - // Compile ExecutionResult compileResult = compile(tempDir); if (compileResult != null) { - return compileResult; // Compilation error + return compileResult; } - // Execute return executeCode(tempDir, inputFile, timeLimitMs, memoryLimitMb); } finally { - // Cleanup temp directory deleteDirectory(tempDir); } } @@ -89,32 +116,42 @@ public ExecutionResult execute(InputStream sourceCode, InputStream testInput, Lo private ExecutionResult compile(Path workDir) { String containerId = null; try { - // Create container for compilation HostConfig hostConfig = HostConfig.newHostConfig() .withBinds(new Bind(workDir.toString(), new Volume("/workspace"))) - .withMemory(512 * 1024 * 1024L) // 512MB for compilation - .withNetworkMode("none"); + .withMemory(512 * 1024 * 1024L) + .withNetworkMode("none") + .withPidsLimit(PIDS_LIMIT) + .withCapDrop(Capability.ALL) + .withReadonlyRootfs(true) + .withMounts(List.of( + new Mount().withType(MountType.TMPFS).withTarget("/tmp") + .withTmpfsOptions(new TmpfsOptions().withSizeBytes(64L * 1024 * 1024).withMode(01777)), + new Mount().withType(MountType.TMPFS).withTarget("/run") + .withTmpfsOptions(new TmpfsOptions().withSizeBytes(8L * 1024 * 1024).withMode(0755)) + )) + .withUlimits(new Ulimit[]{new Ulimit("fsize", 67108864L, 67108864L)}) + .withSecurityOpts(buildSecurityOpts()); CreateContainerResponse container = dockerClient.createContainerCmd(JAVA_IMAGE) .withHostConfig(hostConfig) .withWorkingDir("/workspace") + .withUser("nobody") .withCmd("javac", "Main.java") .exec(); containerId = container.getId(); dockerClient.startContainerCmd(containerId).exec(); - // Wait for compilation int exitCode = dockerClient.waitContainerCmd(containerId) .exec(new WaitContainerResultCallback()) .awaitStatusCode(30, TimeUnit.SECONDS); if (exitCode != 0) { - String stderr = getContainerLogs(containerId, true); + String stderr = getContainerLogsCompile(containerId); return ExecutionResult.compilationError(stderr); } - return null; // Success + return null; } catch (Exception e) { logger.error("Compilation error", e); return ExecutionResult.compilationError("Compilation failed: " + e.getMessage()); @@ -137,9 +174,20 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit HostConfig hostConfig = HostConfig.newHostConfig() .withBinds(new Bind(workDir.toString(), new Volume("/workspace"))) .withMemory(memoryLimitMb * 1024 * 1024L) - .withMemorySwap(memoryLimitMb * 1024 * 1024L) // Disable swap - .withCpuQuota(100000L) // 1 CPU - .withNetworkMode("none"); + .withMemorySwap(memoryLimitMb * 1024 * 1024L) + .withCpuQuota(100000L) + .withNetworkMode("none") + .withPidsLimit(PIDS_LIMIT) + .withCapDrop(Capability.ALL) + .withReadonlyRootfs(true) + .withMounts(List.of( + new Mount().withType(MountType.TMPFS).withTarget("/tmp") + .withTmpfsOptions(new TmpfsOptions().withSizeBytes(32L * 1024 * 1024).withMode(01777)), + new Mount().withType(MountType.TMPFS).withTarget("/run") + .withTmpfsOptions(new TmpfsOptions().withSizeBytes(8L * 1024 * 1024).withMode(0755)) + )) + .withUlimits(new Ulimit[]{new Ulimit("fsize", 33554432L, 33554432L)}) + .withSecurityOpts(buildSecurityOpts()); String[] cmd; if (Files.exists(inputFile) && Files.size(inputFile) > 0) { @@ -151,6 +199,7 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit CreateContainerResponse container = dockerClient.createContainerCmd(JAVA_IMAGE) .withHostConfig(hostConfig) .withWorkingDir("/workspace") + .withUser("nobody") .withCmd(cmd) .exec(); @@ -158,9 +207,8 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit final String finalContainerId = containerId; dockerClient.startContainerCmd(containerId).exec(); - // Wait with timeout ExecutorService executor = Executors.newSingleThreadExecutor(); - Future future = executor.submit(() -> + Future future = executor.submit(() -> dockerClient.waitContainerCmd(finalContainerId) .exec(new WaitContainerResultCallback()) .awaitStatusCode() @@ -179,21 +227,25 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit long executionTime = System.currentTimeMillis() - startTime; - // Check for memory limit (best effort - Docker kills container if exceeded) - if (exitCode == 137) { // SIGKILL - often means OOM + if (exitCode == 137) { return ExecutionResult.memoryLimitExceeded(memoryLimitMb * 1024L); } - String stdout = getContainerLogs(containerId, false); - String stderr = getContainerLogs(containerId, true); + int[] sharedBytes = {0}; + LogResult stdoutResult = getContainerLogsLimited(containerId, false, sharedBytes); + LogResult stderrResult = getContainerLogsLimited(containerId, true, sharedBytes); + + if (stdoutResult.truncated() || stderrResult.truncated()) { + return ExecutionResult.outputLimitExceeded( + stdoutResult.content() + "\n[Output truncated: exceeded 4 MB limit]", executionTime); + } if (exitCode != 0) { - return ExecutionResult.runtimeError(stderr, exitCode, executionTime); + return ExecutionResult.runtimeError(stderrResult.content(), exitCode, executionTime); } Long memoryUsed = getContainerPeakMemory(containerId); - - return ExecutionResult.success(stdout, executionTime, memoryUsed); + return ExecutionResult.success(stdoutResult.content(), executionTime, memoryUsed); } catch (Exception e) { logger.error("Execution error", e); @@ -209,84 +261,128 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit } } - private String getContainerLogs(String containerId, boolean stderr) { + private record LogResult(String content, boolean truncated) {} + + private LogResult getContainerLogsLimited(String containerId, boolean useStderr, int[] sharedBytes) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + boolean[] truncated = {false}; try { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); dockerClient.logContainerCmd(containerId) - .withStdOut(!stderr) - .withStdErr(stderr) - .exec(new Adapter() { - @Override - public void onNext(Frame frame) { - try { - outputStream.write(frame.getPayload()); - } catch (Exception e) { - logger.error("Error reading frame", e); - } + .withStdOut(!useStderr) + .withStdErr(useStderr) + .exec(new ResultCallback.Adapter() { + @Override + public void onNext(Frame frame) { + byte[] payload = frame.getPayload(); + if (payload == null) return; + synchronized (sharedBytes) { + int remaining = OUTPUT_LIMIT_BYTES - sharedBytes[0]; + if (remaining <= 0) { truncated[0] = true; return; } + int toWrite = Math.min(payload.length, remaining); + try { baos.write(payload, 0, toWrite); } catch (Exception ignored) {} + sharedBytes[0] += toWrite; + if (toWrite < payload.length) truncated[0] = true; } - }) - .awaitCompletion(5, TimeUnit.SECONDS); - - return outputStream.toString(StandardCharsets.UTF_8); + } + }).awaitCompletion(5, TimeUnit.SECONDS); } catch (Exception e) { logger.error("Failed to get container logs", e); - return ""; } + return new LogResult(baos.toString(StandardCharsets.UTF_8), truncated[0]); + } + + private String getContainerLogsCompile(String containerId) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int[] bytesWritten = {0}; + try { + dockerClient.logContainerCmd(containerId) + .withStdOut(false) + .withStdErr(true) + .exec(new ResultCallback.Adapter() { + @Override + public void onNext(Frame frame) { + byte[] payload = frame.getPayload(); + if (payload == null) return; + int remaining = COMPILE_STDERR_LIMIT_BYTES - bytesWritten[0]; + if (remaining <= 0) return; + int toWrite = Math.min(payload.length, remaining); + try { baos.write(payload, 0, toWrite); } catch (Exception ignored) {} + bytesWritten[0] += toWrite; + } + }).awaitCompletion(5, TimeUnit.SECONDS); + } catch (Exception e) { + logger.error("Failed to get compile logs", e); + } + return baos.toString(StandardCharsets.UTF_8); + } + + private List buildSecurityOpts() { + List opts = new ArrayList<>(); + opts.add("no-new-privileges:true"); + if (SECCOMP_PROFILE != null) { + opts.add("seccomp=" + SECCOMP_PROFILE); + } + return opts; } private void deleteDirectory(Path directory) { try { - Files.walk(directory) - .sorted((a, b) -> -a.compareTo(b)) - .forEach(path -> { - try { - Files.deleteIfExists(path); - } catch (Exception e) { - logger.warn("Failed to delete: " + path, e); - } - }); + Path realBase = directory.toRealPath(); + Files.walkFileTree(directory, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + try { + if (file.toRealPath().startsWith(realBase)) Files.deleteIfExists(file); + } catch (Exception e) { + logger.warn("Failed to delete: {}", file, e); + } + return FileVisitResult.CONTINUE; + } + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) { + try { Files.deleteIfExists(dir); } catch (Exception e) { logger.warn("Failed to delete dir: {}", dir, e); } + return FileVisitResult.CONTINUE; + } + }); } catch (Exception e) { - logger.warn("Failed to delete directory: " + directory, e); + logger.warn("Failed to delete directory: {}", directory, e); } } private Long getContainerPeakMemory(String containerId) { try { final CountDownLatch latch = new CountDownLatch(1); - final Statistics[] statsHolder = new Statistics[1]; // Array to allow mutation - + final Statistics[] statsHolder = new Statistics[1]; + dockerClient.statsCmd(containerId) - .withNoStream(true) // Single snapshot + .withNoStream(true) .exec(new ResultCallback.Adapter() { @Override public void onNext(Statistics stats) { statsHolder[0] = stats; } - @Override public void onComplete() { latch.countDown(); super.onComplete(); } - @Override public void onError(Throwable throwable) { latch.countDown(); super.onError(throwable); } }); - - // Wait for completion (with timeout) + latch.await(5, TimeUnit.SECONDS); Statistics stats = statsHolder[0]; - + if (stats != null && stats.getMemoryStats() != null) { Long maxUsage = stats.getMemoryStats().getMaxUsage(); - return maxUsage != null ? maxUsage / (1024 * 1024) : 0L; // Bytes to MB + return maxUsage != null ? maxUsage / (1024 * 1024) : 0L; } } catch (Exception e) { logger.warn("Failed to get memory stats for container {}", containerId, e); } - return 0L; // Fallback + return 0L; } } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/python/PythonExecutor.java b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/python/PythonExecutor.java index 360660c..d73eb3c 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/python/PythonExecutor.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/sandbox/python/PythonExecutor.java @@ -7,19 +7,31 @@ import com.github.dockerjava.api.command.CreateContainerResponse; import com.github.dockerjava.api.command.WaitContainerResultCallback; import com.github.dockerjava.api.model.Bind; +import com.github.dockerjava.api.model.Capability; +import com.github.dockerjava.api.model.Frame; import com.github.dockerjava.api.model.HostConfig; +import com.github.dockerjava.api.model.Mount; +import com.github.dockerjava.api.model.MountType; import com.github.dockerjava.api.model.Statistics; +import com.github.dockerjava.api.model.TmpfsOptions; +import com.github.dockerjava.api.model.Ulimit; import com.github.dockerjava.api.model.Volume; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.*; @Component("PYTHON") @@ -29,6 +41,19 @@ public class PythonExecutor implements LanguageExecutor { private static final String PYTHON_IMAGE = "python:3.11-slim"; private static final long DEFAULT_TIME_LIMIT_MS = 5000L; private static final long DEFAULT_MEMORY_LIMIT_MB = 256L; + private static final int OUTPUT_LIMIT_BYTES = 4 * 1024 * 1024; + private static final long PIDS_LIMIT = 64L; + + private static final String SECCOMP_PROFILE; + static { + String profile = null; + try (InputStream is = PythonExecutor.class.getResourceAsStream("/seccomp/sandbox-profile.json")) { + if (is != null) profile = new String(is.readAllBytes(), StandardCharsets.UTF_8); + } catch (Exception e) { + // Runs without seccomp if profile fails to load + } + SECCOMP_PROFILE = profile; + } public PythonExecutor(DockerClient dockerClient) { this.dockerClient = dockerClient; @@ -61,19 +86,21 @@ public ExecutionResult execute(InputStream sourceCode, InputStream testInput, Lo Path inputFile = tempDir.resolve("input.txt"); try { - // Save source code - Files.copy(sourceCode, sourceFile, StandardCopyOption.REPLACE_EXISTING); + Files.setPosixFilePermissions(tempDir, PosixFilePermissions.fromString("rwxrwxrwx")); + + byte[] sourceBytes = sourceCode.readNBytes(512 * 1024); + Files.write(sourceFile, sourceBytes); + Files.setPosixFilePermissions(sourceFile, PosixFilePermissions.fromString("r--r--r--")); - // Save test input if provided if (testInput != null) { - Files.copy(testInput, inputFile, StandardCopyOption.REPLACE_EXISTING); + byte[] inputBytes = testInput.readNBytes(64 * 1024 * 1024); + Files.write(inputFile, inputBytes); + Files.setPosixFilePermissions(inputFile, PosixFilePermissions.fromString("r--r--r--")); } - // Execute (Python doesn't need compilation) return executeCode(tempDir, inputFile, timeLimitMs, memoryLimitMb); } finally { - // Cleanup temp directory deleteDirectory(tempDir); } } @@ -86,9 +113,20 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit HostConfig hostConfig = HostConfig.newHostConfig() .withBinds(new Bind(workDir.toString(), new Volume("/workspace"))) .withMemory(memoryLimitMb * 1024 * 1024L) - .withMemorySwap(memoryLimitMb * 1024 * 1024L) // Disable swap - .withCpuQuota(100000L) // 1 CPU - .withNetworkMode("none"); + .withMemorySwap(memoryLimitMb * 1024 * 1024L) + .withCpuQuota(100000L) + .withNetworkMode("none") + .withPidsLimit(PIDS_LIMIT) + .withCapDrop(Capability.ALL) + .withReadonlyRootfs(true) + .withMounts(List.of( + new Mount().withType(MountType.TMPFS).withTarget("/tmp") + .withTmpfsOptions(new TmpfsOptions().withSizeBytes(32L * 1024 * 1024).withMode(01777)), + new Mount().withType(MountType.TMPFS).withTarget("/run") + .withTmpfsOptions(new TmpfsOptions().withSizeBytes(8L * 1024 * 1024).withMode(0755)) + )) + .withUlimits(new Ulimit[]{new Ulimit("fsize", 33554432L, 33554432L)}) + .withSecurityOpts(buildSecurityOpts()); String[] cmd; if (Files.exists(inputFile) && Files.size(inputFile) > 0) { @@ -100,6 +138,8 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit CreateContainerResponse container = dockerClient.createContainerCmd(PYTHON_IMAGE) .withHostConfig(hostConfig) .withWorkingDir("/workspace") + .withUser("nobody") + .withEnv("PYTHONDONTWRITEBYTECODE=1") .withCmd(cmd) .exec(); @@ -107,9 +147,8 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit final String finalContainerId = containerId; dockerClient.startContainerCmd(containerId).exec(); - // Wait with timeout ExecutorService executor = Executors.newSingleThreadExecutor(); - Future future = executor.submit(() -> + Future future = executor.submit(() -> dockerClient.waitContainerCmd(finalContainerId) .exec(new WaitContainerResultCallback()) .awaitStatusCode() @@ -128,26 +167,29 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit long executionTime = System.currentTimeMillis() - startTime; - // Check for memory limit (Docker kills container if exceeded) - if (exitCode == 137) { // SIGKILL - often means OOM + if (exitCode == 137) { return ExecutionResult.memoryLimitExceeded(memoryLimitMb * 1024L); } - String stdout = getContainerLogs(containerId, false); - String stderr = getContainerLogs(containerId, true); + int[] sharedBytes = {0}; + LogResult stdoutResult = getContainerLogsLimited(containerId, false, sharedBytes); + LogResult stderrResult = getContainerLogsLimited(containerId, true, sharedBytes); - // Python syntax errors will exit with non-zero code - if (exitCode != 0 && stderr.contains("SyntaxError")) { - return ExecutionResult.compilationError(stderr); + if (stdoutResult.truncated() || stderrResult.truncated()) { + return ExecutionResult.outputLimitExceeded( + stdoutResult.content() + "\n[Output truncated: exceeded 4 MB limit]", executionTime); + } + + if (exitCode != 0 && stderrResult.content().contains("SyntaxError")) { + return ExecutionResult.compilationError(stderrResult.content()); } if (exitCode != 0) { - return ExecutionResult.runtimeError(stderr, exitCode, executionTime); + return ExecutionResult.runtimeError(stderrResult.content(), exitCode, executionTime); } Long memoryUsed = getContainerPeakMemory(containerId); - - return ExecutionResult.success(stdout, executionTime, memoryUsed); + return ExecutionResult.success(stdoutResult.content(), executionTime, memoryUsed); } catch (Exception e) { logger.error("Execution error", e); @@ -163,44 +205,66 @@ private ExecutionResult executeCode(Path workDir, Path inputFile, long timeLimit } } - private String getContainerLogs(String containerId, boolean stderr) { + private record LogResult(String content, boolean truncated) {} + + private LogResult getContainerLogsLimited(String containerId, boolean useStderr, int[] sharedBytes) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + boolean[] truncated = {false}; try { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); dockerClient.logContainerCmd(containerId) - .withStdOut(!stderr) - .withStdErr(stderr) - .exec(new com.github.dockerjava.api.async.ResultCallback.Adapter() { - @Override - public void onNext(com.github.dockerjava.api.model.Frame frame) { - try { - outputStream.write(frame.getPayload()); - } catch (Exception e) { - logger.error("Error reading frame", e); - } + .withStdOut(!useStderr) + .withStdErr(useStderr) + .exec(new ResultCallback.Adapter() { + @Override + public void onNext(Frame frame) { + byte[] payload = frame.getPayload(); + if (payload == null) return; + synchronized (sharedBytes) { + int remaining = OUTPUT_LIMIT_BYTES - sharedBytes[0]; + if (remaining <= 0) { truncated[0] = true; return; } + int toWrite = Math.min(payload.length, remaining); + try { baos.write(payload, 0, toWrite); } catch (Exception ignored) {} + sharedBytes[0] += toWrite; + if (toWrite < payload.length) truncated[0] = true; } - }) - .awaitCompletion(5, TimeUnit.SECONDS); - - return outputStream.toString(StandardCharsets.UTF_8); + } + }).awaitCompletion(5, TimeUnit.SECONDS); } catch (Exception e) { logger.error("Failed to get container logs", e); - return ""; } + return new LogResult(baos.toString(StandardCharsets.UTF_8), truncated[0]); + } + + private List buildSecurityOpts() { + List opts = new ArrayList<>(); + opts.add("no-new-privileges:true"); + if (SECCOMP_PROFILE != null) { + opts.add("seccomp=" + SECCOMP_PROFILE); + } + return opts; } private void deleteDirectory(Path directory) { try { - Files.walk(directory) - .sorted((a, b) -> -a.compareTo(b)) - .forEach(path -> { - try { - Files.deleteIfExists(path); - } catch (Exception e) { - logger.warn("Failed to delete: " + path, e); - } - }); + Path realBase = directory.toRealPath(); + Files.walkFileTree(directory, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + try { + if (file.toRealPath().startsWith(realBase)) Files.deleteIfExists(file); + } catch (Exception e) { + logger.warn("Failed to delete: {}", file, e); + } + return FileVisitResult.CONTINUE; + } + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) { + try { Files.deleteIfExists(dir); } catch (Exception e) { logger.warn("Failed to delete dir: {}", dir, e); } + return FileVisitResult.CONTINUE; + } + }); } catch (Exception e) { - logger.warn("Failed to delete directory: " + directory, e); + logger.warn("Failed to delete directory: {}", directory, e); } } @@ -208,39 +272,36 @@ private Long getContainerPeakMemory(String containerId) { try { final CountDownLatch latch = new CountDownLatch(1); final Statistics[] statsHolder = new Statistics[1]; - + dockerClient.statsCmd(containerId) - .withNoStream(true) // Single snapshot + .withNoStream(true) .exec(new ResultCallback.Adapter() { @Override public void onNext(Statistics stats) { statsHolder[0] = stats; } - @Override public void onComplete() { latch.countDown(); super.onComplete(); } - @Override public void onError(Throwable throwable) { latch.countDown(); super.onError(throwable); } }); - - // Wait for completion (with timeout) + latch.await(5, TimeUnit.SECONDS); Statistics stats = statsHolder[0]; - + if (stats != null && stats.getMemoryStats() != null) { Long maxUsage = stats.getMemoryStats().getMaxUsage(); - return maxUsage != null ? maxUsage / (1024 * 1024) : 0L; // Bytes to MB + return maxUsage != null ? maxUsage / (1024 * 1024) : 0L; } } catch (Exception e) { logger.warn("Failed to get memory stats for container {}", containerId, e); } - return 0L; // Fallback + return 0L; } } diff --git a/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java b/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java index 6ffd55e..2c207b0 100644 --- a/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java +++ b/codehive-worker/src/main/java/com/github/codehive/worker/service/TestExecutionService.java @@ -274,6 +274,8 @@ private String getStatusFeedback(ExecutionStatus status) { return "Time limit exceeded"; case MLE: return "Memory limit exceeded"; + case OLE: + return "Output limit exceeded"; case RTE: return "Runtime error"; case CE: diff --git a/codehive-worker/src/main/resources/seccomp/sandbox-profile.json b/codehive-worker/src/main/resources/seccomp/sandbox-profile.json new file mode 100644 index 0000000..f01ca8d --- /dev/null +++ b/codehive-worker/src/main/resources/seccomp/sandbox-profile.json @@ -0,0 +1,59 @@ +{ + "defaultAction": "SCMP_ACT_ALLOW", + "syscalls": [ + { + "names": [ + "unshare", + "keyctl", + "add_key", + "request_key", + "ptrace", + "perf_event_open", + "bpf", + "userfaultfd", + "process_vm_readv", + "process_vm_writev", + "kcmp", + "syslog", + "acct", + "pivot_root", + "chroot", + "mount", + "umount2", + "swapon", + "swapoff", + "reboot", + "kexec_load", + "kexec_file_load", + "init_module", + "finit_module", + "delete_module", + "io_uring_setup", + "io_uring_enter", + "io_uring_register", + "io_uring_sqe_walk", + "create_module", + "query_module", + "get_kernel_syms", + "nfsservctl", + "lookup_dcookie", + "fanotify_init" + ], + "action": "SCMP_ACT_ERRNO", + "errnoRet": 1 + }, + { + "names": ["clone"], + "action": "SCMP_ACT_ERRNO", + "errnoRet": 1, + "args": [ + { + "index": 0, + "value": 268435456, + "op": "SCMP_CMP_MASKED_EQ", + "valueTwo": 268435456 + } + ] + } + ] +} diff --git a/llms/backend/auth/README.md b/llms/backend/auth/README.md index 9af84de..d521292 100644 --- a/llms/backend/auth/README.md +++ b/llms/backend/auth/README.md @@ -8,6 +8,7 @@ This document explains the authentication and user registration behavior impleme - POST /api/auth/signup - POST /api/auth/signup/csv - GET /api/auth/me +- PUT /api/auth/me/password Controller: codehive-backend/src/main/java/com/github/codehive/controller/AuthController.java @@ -44,10 +45,17 @@ Important validation rules: - Duplicate email/enrollment is blocked both in-file and in-database. ## Current User (/me) -1. Controller reads Authorization header. -2. Bearer token is extracted. -3. AuthService resolves user from token subject. -4. UserDTO is returned. +1. Spring Security's `JWTAuthenticationFilter` runs before the controller and populates `SecurityContextHolder`. +2. Controller receives the already-authenticated `Authentication` object as a method argument. +3. `authentication.getName()` returns the subject email from the validated JWT. +4. `AuthService.getUserByEmail(email)` loads and returns the UserDTO. + +Do NOT re-parse the `Authorization` header in controller methods — the `Authentication` injection already provides the verified identity. + +## Update Password (/me/password) +1. Same `Authentication` injection as `/me` to identify the caller. +2. `UpdatePasswordRequest` carries `currentPassword` and `newPassword`. +3. `AuthService.updatePassword(userId, request)` verifies current password, encodes and persists new password, and sets `temporaryPassword = false`. ## Data and Contracts Request classes: @@ -67,6 +75,13 @@ User storage model: - Validation errors are handled by GlobalExceptionHandler. - Endpoint rate limits are enforced via @RateLimit. +## AuthService Key Methods +- `login(LoginRequest)` — authenticate by email or enrollment number, return JWT + UserDTO +- `register(SignUpRequest)` — admin-only registration; generates temporary password and sends welcome email +- `getUserByEmail(String email)` — load UserDTO from email; used by controllers receiving `Authentication` +- `updatePassword(UUID userId, UpdatePasswordRequest)` — verify current password and persist new one +- `generateToken(User)` — build JWT with `userId` and `role` claims, `email` as subject + ## Notes for Future Changes - If role/permission model expands, update JWT claims and authorities mapping. - Keep LoginRequest backward compatible because frontend and clients depend on identifier semantics. diff --git a/llms/backend/controller/README.md b/llms/backend/controller/README.md index 9960ba0..0e37095 100644 --- a/llms/backend/controller/README.md +++ b/llms/backend/controller/README.md @@ -37,6 +37,7 @@ Error handling is centralized in: ### CheckExecutionController - `POST /api/execution/check` — creates execution and queues worker job. - `GET /api/execution/check/{id}` — polls execution status. +- `GET /api/execution/check/{id}/report` — fetches the full execution report JSON from MinIO (per-test-case results, timing, memory, feedback). Returns 404 if the execution is still PENDING or the report is not available yet. ### AssignmentController - `POST /api/assignments` — multipart; creates assignment, uploads files, queues test generation. diff --git a/llms/backend/executions/README.md b/llms/backend/executions/README.md index 18e481d..3beb857 100644 --- a/llms/backend/executions/README.md +++ b/llms/backend/executions/README.md @@ -47,6 +47,10 @@ The API returns 202 Accepted immediately — output generation is asynchronous. 3. ExecutionResultService loads execution by id. 4. Status, timeMs, and memoryMb are updated in the executions table. 5. Client retrieves updated status via `GET /api/execution/check/{id}`. +6. Client retrieves the full per-test-case report via `GET /api/execution/check/{id}/report`. + - Fetches `report.json` from MinIO at `test-execution/execution-{id}/output/report.json`. + - Returns 404 with a clear message when execution is still PENDING. + - Deserializes into `ExecutionReport` using Jackson ObjectMapper. ## Key Data Contracts Assignment creation request: @@ -64,6 +68,9 @@ Queue payloads: API polling response: - model/dto/ExecutionDTO.java +Full report response (from MinIO): +- model/dto/queue/ExecutionReport.java (deserialized from JSON stored by worker) + ## Execution Job Construction PRACTICE mode: - Loads first ReferenceSolution for the assignment to get language and reference path. diff --git a/llms/backend/model/README.md b/llms/backend/model/README.md index 2dd509b..790bc0e 100644 --- a/llms/backend/model/README.md +++ b/llms/backend/model/README.md @@ -120,7 +120,7 @@ Enums are persisted and transferred as string values: - Role, Scope - Language (JAVA, PYTHON, C, CPP) - ExecutionType (PRACTICE, DEFINITIVE) -- ExecutionStatus (AC, WA, CE, RTE, TLE, MLE, PENDING) +- ExecutionStatus (AC, WA, CE, RTE, TLE, MLE, OLE, PENDING) — OLE = Output Limit Exceeded (> 4 MB combined stdout+stderr) - ComparatorType (EXACT_MATCH, FLOATING_POINT) ## Exception Model diff --git a/llms/backend/security/README.md b/llms/backend/security/README.md index 7136c3b..c5ff5b2 100644 --- a/llms/backend/security/README.md +++ b/llms/backend/security/README.md @@ -33,10 +33,9 @@ Configured in SecurityConfig: - Permit all: - POST /api/auth/login - POST /api/recovery-password/** - - /api/execution/** (currently open) - /ws/** - Swagger endpoints -- All other routes require authentication. +- All other routes (including `/api/execution/**`) require authentication via `anyRequest().authenticated()`. Method-level restrictions: - Admin-only actions use @PreAuthorize("hasAuthority('ADMIN')"). @@ -57,6 +56,16 @@ Security-related exceptions are translated by GlobalExceptionHandler: - AccessDeniedException -> 403 ## Hardening Notes -- /api/execution/** is currently permitAll and should be reviewed if requester identity becomes mandatory. - CORS allow-all is convenient for development but should be narrowed for production. - Keep JWT secret/expiration in environment configuration and rotate when required. + +## Controller Identity Pattern +Controllers that need the authenticated user's identity must inject `Authentication` as a parameter — never re-parse the `Authorization` header manually. The filter already validated and stored the principal in `SecurityContextHolder` before the controller is invoked. + +```java +@GetMapping("/me") +public ResponseEntity me(Authentication authentication) { + UserDTO user = authService.getUserByEmail(authentication.getName()); + ... +} +``` diff --git a/llms/backend/service/README.md b/llms/backend/service/README.md index 0b52062..845714a 100644 --- a/llms/backend/service/README.md +++ b/llms/backend/service/README.md @@ -24,7 +24,12 @@ Responsibilities: - Login using email or enrollment number identifier. - Admin-driven user registration. - CSV batch registration support. -- JWT issuance and user retrieval by token. +- JWT issuance and user retrieval by token or email. +- Password updates (verify current, encode new, clear temporaryPassword flag). + +Key methods: +- `getUserByEmail(email)` — load UserDTO from email; used by controllers that receive `Authentication` +- `updatePassword(userId, request)` — verifies current password and persists the new one Key dependencies: - UserRepository, PasswordEncoder, JwtUtil, MailSenderService @@ -69,10 +74,16 @@ Responsibilities: - Create Execution entity. - Upload source code to MinIO. - Build and publish ExecutionJob to `codehive_queue`. +- Fetch and deserialize the execution report JSON from MinIO. + +Key methods: +- `requestExecution(ExecutionRequest)` — full submission flow; returns ExecutionDTO for polling +- `getExecutionById(UUID)` — status polling +- `getExecutionReport(UUID)` — downloads `report.json` from MinIO and deserializes via ObjectMapper; returns 404 if pending or not found Key dependencies: - ExecutionRepository, AssignmentRepository, ReferenceSolutionRepository, TestCaseRepository -- ObjectStorageService, ExecutionRequestProducer, UserRepository +- ObjectStorageService, ExecutionRequestProducer, UserRepository, ObjectMapper PRACTICE mode: uses inline testCases, resolves reference solution from DB. DEFINITIVE mode: numTests counted from TestCaseRepository, no reference solution needed at runtime. diff --git a/llms/worker/OVERVIEW.md b/llms/worker/OVERVIEW.md index 15ba49c..d04510b 100644 --- a/llms/worker/OVERVIEW.md +++ b/llms/worker/OVERVIEW.md @@ -34,6 +34,18 @@ Safety and reliability concerns: - Compilation gate aborts both pipelines early on CE. - Results (including failures) are always published — no silent job loss. +Container security hardening (all executors): +- PID limit prevents fork bombs (64 for JVM/Python, 32 for C/C++). +- `Capability.ALL` dropped — containers have no Linux capabilities. +- Read-only root filesystem with tmpfs at `/tmp` and `/run`. +- `nobody` user — containers never run as root. +- Custom seccomp profile blocks dangerous syscalls (ptrace, bpf, io_uring, clone+NEWUSER, etc.). +- `no-new-privileges:true` security option. +- fsize ulimit prevents disk exhaustion. +- Combined stdout+stderr capped at 4 MB — excess returns OLE verdict. + +Execution verdicts: AC, WA, TLE, MLE, OLE (output limit exceeded), RTE, CE, PENDING. + ## Useful Commands Run these from codehive-worker. diff --git a/llms/worker/sandbox/README.md b/llms/worker/sandbox/README.md index fe83d30..03bd82e 100644 --- a/llms/worker/sandbox/README.md +++ b/llms/worker/sandbox/README.md @@ -1,7 +1,7 @@ # Worker Sandbox Implementation ## Scope -This document explains how language executors run code in isolated Docker containers with time and memory constraints. +This document explains how language executors run code in isolated Docker containers with full security hardening. Core abstraction: - codehive-worker/src/main/java/com/github/codehive/worker/sandbox/LanguageExecutor.java @@ -17,56 +17,75 @@ Executors: ## Executor Contract LanguageExecutor.execute arguments: -- sourceCode input stream -- testInput input stream (nullable) +- sourceCode input stream (capped at 512 KB before writing to disk) +- testInput input stream (nullable; capped at 64 MB before writing to disk) - timeLimitMs - memoryLimitMb Returns ExecutionResult with: -- status +- status (AC, WA, TLE, MLE, OLE, RTE, CE, PENDING) - output / errorOutput - executionTimeMs - memoryUsedMb - exitCode - compilationError (when applicable) -## Isolation Strategy -Each execution uses temporary filesystem workspace and Docker container isolation. +## Container Security Hardening +All containers (both compile and execute phases) apply these restrictions: -Container restrictions: -- no network access (networkMode none) -- memory hard limit and swap disabled -- CPU quota for bounded CPU usage +| Restriction | Detail | +|---|---| +| Network | `networkMode("none")` — no outbound access | +| Memory | Hard limit + swap disabled (`memorySwap = memory`) | +| CPU | 1 vCPU (`cpuQuota = 100000`) | +| PIDs | 64 for Java/Python, 32 for C/C++ — prevents fork bombs | +| Capabilities | `withCapDrop(Capability.ALL)` — no Linux capabilities | +| Root filesystem | `withReadonlyRootfs(true)` — no writes outside /workspace and /tmp | +| tmpfs /tmp | 64 MB (compile), 32 MB (execute) — satisfies runtime scratch needs | +| tmpfs /run | 8 MB — for runtime lock files | +| File size ulimit | 64 MB per file (compile), 32 MB per file (execute) — prevents disk exhaustion | +| No privilege gain | `no-new-privileges:true` in security opts | +| Seccomp | Custom profile at `resources/seccomp/sandbox-profile.json` | +| User | `withUser("nobody")` — containers never run as root | -Timeout control: -- WaitContainer uses Future timeout based on timeLimitMs. -- timeout leads to TLE. +Seccomp profile blocks: `unshare`, `ptrace`, `bpf`, `io_uring_*`, `keyctl`, `mount`, `reboot`, kernel module syscalls, and `clone` with `CLONE_NEWUSER` flag. -Memory limit signaling: -- exit code 137 is interpreted as likely OOM and mapped to MLE. +Temp directory permissions are set to `rwxrwxrwx` so `nobody` can access the workspace. Source and input files are set to `r--r--r--`. + +## Output Size Limit (OLE verdict) +stdout and stderr are captured with a shared 4 MB cap across both streams. When exceeded the executor returns `ExecutionResult.outputLimitExceeded()` with status `OLE`. + +Compilation stderr is separately capped at 256 KB. + +The `getContainerLogsLimited` method in each executor implements this via a synchronized shared byte counter in the Docker log callback. + +## Timeout and Memory Signals +- Timeout: Future.get(timeLimitMs + 1000) → TLE on TimeoutException. +- Memory: exit code 137 (SIGKILL from OOM killer) → MLE. ## Language-Specific Behavior Java: -- compile step with javac Main.java -- run step with java Main +- compile: `javac Main.java` (512 MB memory, 64 PID limit) +- execute: `java Main` or `java Main < input.txt` Python: -- no explicit compile step -- syntax errors are treated as CE when stderr includes SyntaxError +- no compile phase +- syntax errors (SyntaxError in stderr) → CE +- `PYTHONDONTWRITEBYTECODE=1` prevents `__pycache__` writes C: -- compile with gcc -o program main.c -lm -- run compiled binary +- compile: `gcc -o program main.c -lm` +- execute: `./program` or `./program < input.txt` C++: -- compile with g++ -o program main.cpp -std=c++17 -lm -- run compiled binary +- compile: `g++ -o program main.cpp -std=c++17 -lm` +- execute: `./program` or `./program < input.txt` All executors: -- optionally pipe input.txt when test input exists -- capture stdout and stderr from container logs -- attempt to read peak memory via Docker stats snapshot -- clean up container and temporary directory in finally blocks +- optionally pipe input.txt when test input is non-empty +- capture stdout/stderr with OLE check +- read peak memory via Docker stats snapshot after container exits +- clean up container and temporary directory in finally blocks using symlink-safe `walkFileTree` deletion ## Image Management On executor initialization: @@ -78,14 +97,21 @@ Default images: - Python: python:3.11-slim - C/C++: gcc:latest +## Seccomp Profile +File: `codehive-worker/src/main/resources/seccomp/sandbox-profile.json` +- defaultAction: SCMP_ACT_ALLOW (permissive base — Docker restrictions remain active) +- Explicitly blocks all dangerous syscalls with `SCMP_ACT_ERRNO` +- Loaded once at class initialization via static block; if loading fails the container runs without it (logged as warning) + ## Factory Resolution LanguageExecutorFactory receives Spring component map keyed by bean names. - Bean names are language enum names (JAVA, PYTHON, C, CPP). - Unknown or null language throws IllegalArgumentException. ## Extension Guidance -- To add a new language: - 1. implement LanguageExecutor - 2. register Spring component with enum-name bean key - 3. extend Language enum and backend mapping - 4. verify ExecutionJob contract supports new language +To add a new language: +1. Implement LanguageExecutor with the same security HostConfig pattern. +2. Register Spring component with enum-name bean key. +3. Extend Language enum and backend mapping. +4. Choose appropriate PIDS_LIMIT (64 for JVM/interpreter, 32 for native). +5. Verify ExecutionJob contract supports new language. From 1d8b617962a11b4318357180bc477eec387f22fb Mon Sep 17 00:00:00 2001 From: IrminDev Date: Mon, 11 May 2026 22:24:00 -0600 Subject: [PATCH 19/32] Added enrollment number validation and minor fixes --- codehive-backend/scripts/fake_users.csv | 10 ++ .../scripts/generate_fake_users.py | 135 ++++++++++++++++++ .../model/request/auth/SignUpRequest.java | 5 + .../github/codehive/service/AuthService.java | 16 ++- .../service/CsvRegistrationService.java | 5 + .../com/github/codehive/utils/JwtUtil.java | 24 ++-- 6 files changed, 183 insertions(+), 12 deletions(-) create mode 100644 codehive-backend/scripts/fake_users.csv create mode 100644 codehive-backend/scripts/generate_fake_users.py diff --git a/codehive-backend/scripts/fake_users.csv b/codehive-backend/scripts/fake_users.csv new file mode 100644 index 0000000..8296d55 --- /dev/null +++ b/codehive-backend/scripts/fake_users.csv @@ -0,0 +1,10 @@ +TEACHER,Karla,Torres,Chavez,2023630627,karla.torres@ittepic.edu.mx +TEACHER,Elena,Torres,Chavez,1998630022,elena.torres@yahoo.com +STUDENT,Luis,Gonzalez,Ramirez,1995630277,luis.gonzalez@ittepic.edu.mx +TEACHER,Sergio,Morales,Flores,1999630615,sergio.morales@gmail.com +STUDENT,Jorge,Cruz,Jimenez,1998630149,jorge.cruz@ittepic.edu.mx +TEACHER,Eduardo,Aguilar,Castillo,2004630352,eduardo.aguilar@hotmail.com +STUDENT,Andres,Castillo,Gutierrez,2011630950,andres.castillo@hotmail.com +STUDENT,Ricardo,Martinez,Gonzalez,2001630813,ricardo.martinez@hotmail.com +STUDENT,Alejandra,Gomez,Silva,2011630694,alejandra.gomez@ittepic.edu.mx +STUDENT,Elena,Silva,Ramirez,1998630419,elena.silva@outlook.com diff --git a/codehive-backend/scripts/generate_fake_users.py b/codehive-backend/scripts/generate_fake_users.py new file mode 100644 index 0000000..cebe352 --- /dev/null +++ b/codehive-backend/scripts/generate_fake_users.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +""" +Generates a CSV file with fake user data for CodeHive bulk registration. +CSV format: role, name, father_last_name, mother_last_name, enrollment_number, email +""" + +import csv +import random +import string +import sys +from datetime import datetime + +FIRST_NAMES = [ + "Carlos", "Maria", "Jose", "Ana", "Luis", "Laura", "Miguel", "Sofia", + "Jorge", "Elena", "Ricardo", "Valeria", "Fernando", "Gabriela", "Andres", + "Patricia", "Diego", "Monica", "Roberto", "Alejandra", "Eduardo", "Claudia", + "Sergio", "Natalia", "Pablo", "Daniela", "Ivan", "Karla", "Oscar", "Veronica", +] + +LAST_NAMES = [ + "Garcia", "Martinez", "Lopez", "Hernandez", "Gonzalez", "Perez", "Rodriguez", + "Sanchez", "Ramirez", "Torres", "Flores", "Rivera", "Gomez", "Diaz", "Cruz", + "Morales", "Reyes", "Gutierrez", "Ortiz", "Chavez", "Romero", "Jimenez", + "Vargas", "Mendoza", "Ruiz", "Aguilar", "Medina", "Castillo", "Herrera", "Silva", +] + +EMAIL_DOMAINS = ["gmail.com", "hotmail.com", "outlook.com", "yahoo.com", "ittepic.edu.mx"] + +CURRENT_YEAR = datetime.now().year + + +def random_name(): + return random.choice(FIRST_NAMES) + + +def random_last_name(): + return random.choice(LAST_NAMES) + + +def generate_enrollment_number(used: set) -> str: + while True: + year = random.randint(1994, CURRENT_YEAR) + suffix = random.randint(0, 999) + number = f"{year}630{suffix:03d}" + if number not in used: + used.add(number) + return number + + +def generate_email(name: str, father_last: str, used: set) -> str: + base = f"{name.lower()}.{father_last.lower()}" + domain = random.choice(EMAIL_DOMAINS) + candidate = f"{base}@{domain}" + if candidate not in used: + used.add(candidate) + return candidate + # Append random digits to avoid collision + while True: + candidate = f"{base}{random.randint(1, 9999)}@{domain}" + if candidate not in used: + used.add(candidate) + return candidate + + +def generate_users(n_admins: int, n_teachers: int, n_students: int) -> list[dict]: + used_enrollments: set = set() + used_emails: set = set() + users = [] + + roles = ( + [("ADMIN", n_admins), ("TEACHER", n_teachers), ("STUDENT", n_students)] + ) + + for role, count in roles: + for _ in range(count): + name = random_name() + father_last = random_last_name() + mother_last = random_last_name() + enrollment = generate_enrollment_number(used_enrollments) + email = generate_email(name, father_last, used_emails) + users.append({ + "role": role, + "name": name, + "father_last_name": father_last, + "mother_last_name": mother_last, + "enrollment_number": enrollment, + "email": email, + }) + + random.shuffle(users) + return users + + +def main(): + print("CodeHive fake user CSV generator") + print("-" * 35) + + try: + n_admins = int(input("Number of admins (n1): ").strip()) + n_teachers = int(input("Number of teachers (n2): ").strip()) + n_students = int(input("Number of students (n3): ").strip()) + except ValueError: + print("Error: all inputs must be integers.", file=sys.stderr) + sys.exit(1) + + if any(v < 0 for v in (n_admins, n_teachers, n_students)): + print("Error: counts must be non-negative.", file=sys.stderr) + sys.exit(1) + + total = n_admins + n_teachers + n_students + if total == 0: + print("Nothing to generate.") + sys.exit(0) + + output_file = "fake_users.csv" + users = generate_users(n_admins, n_teachers, n_students) + + with open(output_file, "w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + for u in users: + writer.writerow([ + u["role"], + u["name"], + u["father_last_name"], + u["mother_last_name"], + u["enrollment_number"], + u["email"], + ]) + + print(f"\nGenerated {total} users ({n_admins} admins, {n_teachers} teachers, {n_students} students)") + print(f"Saved to: {output_file}") + + +if __name__ == "__main__": + main() diff --git a/codehive-backend/src/main/java/com/github/codehive/model/request/auth/SignUpRequest.java b/codehive-backend/src/main/java/com/github/codehive/model/request/auth/SignUpRequest.java index 29eba8e..bb33c10 100644 --- a/codehive-backend/src/main/java/com/github/codehive/model/request/auth/SignUpRequest.java +++ b/codehive-backend/src/main/java/com/github/codehive/model/request/auth/SignUpRequest.java @@ -5,6 +5,7 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; public class SignUpRequest { @@ -24,6 +25,10 @@ public class SignUpRequest { private String motherLastName; @NotBlank(message = "Enrollment number is required") + @Pattern( + regexp = "^(199[4-9]|[2-9]\\d{3})630\\d{3}$", + message = "Enrollment number must be 10 digits: year (>=1994), followed by 630, followed by any 3 digits" + ) private String enrollmentNumber; @NotBlank(message = "Email is required") diff --git a/codehive-backend/src/main/java/com/github/codehive/service/AuthService.java b/codehive-backend/src/main/java/com/github/codehive/service/AuthService.java index 5bfeb8b..62659c6 100644 --- a/codehive-backend/src/main/java/com/github/codehive/service/AuthService.java +++ b/codehive-backend/src/main/java/com/github/codehive/service/AuthService.java @@ -14,6 +14,7 @@ import java.util.UUID; import java.util.regex.Pattern; +import io.jsonwebtoken.ExpiredJwtException; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVRecord; @@ -217,10 +218,17 @@ public CsvBulkRegisterResponse registerFromCsv(MultipartFile file) throws IOExce } public UserDTO getUserByToken(String token) { - String email = jwtUtil.extractClaim(token, claims -> claims.getSubject()); - User user = userRepository.findByEmail(email) - .orElseThrow(() -> new IncorrectCredentialsException("User not found")); - return UserMapper.toDTO(user); + try { + if (jwtUtil.isTokenExpired(token)) { + throw new IncorrectCredentialsException("Token has expired"); + } + String email = jwtUtil.extractClaim(token, claims -> claims.getSubject()); + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new IncorrectCredentialsException("User not found")); + return UserMapper.toDTO(user); + } catch (ExpiredJwtException e) { + throw new IncorrectCredentialsException("Token has expired"); + } } @Transactional(readOnly = true) diff --git a/codehive-backend/src/main/java/com/github/codehive/service/CsvRegistrationService.java b/codehive-backend/src/main/java/com/github/codehive/service/CsvRegistrationService.java index be43e73..f4dce81 100644 --- a/codehive-backend/src/main/java/com/github/codehive/service/CsvRegistrationService.java +++ b/codehive-backend/src/main/java/com/github/codehive/service/CsvRegistrationService.java @@ -37,6 +37,9 @@ public class CsvRegistrationService { private static final Pattern EMAIL_PATTERN = Pattern.compile( "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"); + private static final Pattern ENROLLMENT_PATTERN = Pattern.compile( + "^(199[4-9]|[2-9]\\d{3})630\\d{3}$"); + private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final MailSenderService mailSenderService; @@ -140,6 +143,8 @@ private String processRow(CSVRecord record, int rowNumber, if (fatherLastName.isEmpty()) rowErrors.add("father last name is empty"); if (motherLastName.isEmpty()) rowErrors.add("mother last name is empty"); if (enrollmentNumber.isEmpty()) rowErrors.add("enrollment number is empty"); + if (!enrollmentNumber.isEmpty() && !ENROLLMENT_PATTERN.matcher(enrollmentNumber).matches()) + rowErrors.add("enrollment number is invalid (must be 10 digits: year >=1994, then 630, then 3 digits)"); if (email.isEmpty()) rowErrors.add("email is empty"); if (!email.isEmpty() && !EMAIL_PATTERN.matcher(email).matches()) rowErrors.add("email is invalid"); diff --git a/codehive-backend/src/main/java/com/github/codehive/utils/JwtUtil.java b/codehive-backend/src/main/java/com/github/codehive/utils/JwtUtil.java index a458175..8d8d4fc 100644 --- a/codehive-backend/src/main/java/com/github/codehive/utils/JwtUtil.java +++ b/codehive-backend/src/main/java/com/github/codehive/utils/JwtUtil.java @@ -41,17 +41,25 @@ private SecretKey getSignInKey() { return Keys.hmacShaKeyFor(keyBytes); } - public T extractClaim(String token, Function claimsResolver){ - final Claims claims = extractAllClaims(token); - return claimsResolver.apply(claims); + public T extractClaim(String token, Function claimsResolver) { + try { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } catch (ExpiredJwtException e) { + throw e; + } } private Claims extractAllClaims(String token) { - return Jwts.parser() - .verifyWith(getSignInKey()) - .build() - .parseSignedClaims(token) - .getPayload(); + try { + return Jwts.parser() + .verifyWith(getSignInKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } catch (ExpiredJwtException e) { + throw e; + } } public boolean isTokenValid(String token, String email){ From 86b932b6c52b3f23d12b712e33065e88622deec0 Mon Sep 17 00:00:00 2001 From: Rodolfo-rgb Date: Tue, 12 May 2026 13:23:29 -0600 Subject: [PATCH 20/32] redesigne of login, forgot-password pages, index Hero.tsx. and implement sandbox monako --- codehive-frontend/app/core/router/routes.ts | 2 + .../app/features/auth/pages/LoginPage.tsx | 456 +++++---- .../auth/pages/RecoveryPasswordPage.tsx | 579 +++++------ .../features/landing/components/Contact.tsx | 17 +- .../features/landing/components/Footer.tsx | 35 +- .../app/features/landing/components/Hero.tsx | 6 +- .../landing/components/HowItWorks.tsx | 4 +- .../practice-workspace/api/execution.api.ts | 5 + .../components/editor/CodeEditor.tsx | 28 + .../components/editor/EditorToolbar.tsx | 42 + .../components/editor/LanguageSelector.tsx | 36 + .../components/layout/ResizablePanels.tsx | 66 ++ .../components/layout/WorkspaceHeader.tsx | 23 + .../components/layout/WorkspaceLayout.tsx | 25 + .../components/problem/ConstraintsSection.tsx | 22 + .../components/problem/ExamplesSection.tsx | 38 + .../components/problem/ProblemDescription.tsx | 39 + .../components/problem/TagsSection.tsx | 25 + .../components/submission/RunButton.tsx | 16 + .../components/submission/SubmitButton.tsx | 16 + .../components/testcase/AddTestCaseButton.tsx | 16 + .../components/testcase/TestCaseEditor.tsx | 29 + .../components/testcase/TestCasePanel.tsx | 48 + .../components/testcase/TestCaseTabs.tsx | 53 + .../app/features/practice-workspace/data.ts | 76 ++ .../hooks/useDraftPersistence.ts | 28 + .../practice-workspace/hooks/useExecution.ts | 48 + .../hooks/useMonacoConfig.ts | 26 + .../pages/PracticeWorkspacePage.tsx | 19 + .../app/features/practice-workspace/routes.ts | 5 + .../routes/practice-workspace.tsx | 19 + .../services/draft.service.ts | 22 + .../services/execution.service.ts | 41 + .../store/workspace.store.ts | 118 +++ .../practice-workspace/types/draft.types.ts | 9 + .../practice-workspace/types/editor.types.ts | 3 + .../types/execution.types.ts | 20 + .../types/testcase.types.ts | 1 + .../app/shared/components/ui/Badge.tsx | 31 + .../app/shared/components/ui/Button.tsx | 55 + .../app/shared/components/ui/Select.tsx | 150 +++ .../app/shared/components/ui/Separator.tsx | 22 + .../app/shared/components/ui/Tabs.tsx | 53 + .../app/shared/components/ui/Textarea.tsx | 20 + codehive-frontend/app/shared/lib/utils.ts | 6 + codehive-frontend/app/styles/global.css | 67 ++ codehive-frontend/package-lock.json | 956 +++++++++++++++++- codehive-frontend/package.json | 13 +- codehive-frontend/react-router.config.ts | 5 +- 49 files changed, 2856 insertions(+), 583 deletions(-) create mode 100644 codehive-frontend/app/features/practice-workspace/api/execution.api.ts create mode 100644 codehive-frontend/app/features/practice-workspace/components/editor/CodeEditor.tsx create mode 100644 codehive-frontend/app/features/practice-workspace/components/editor/EditorToolbar.tsx create mode 100644 codehive-frontend/app/features/practice-workspace/components/editor/LanguageSelector.tsx create mode 100644 codehive-frontend/app/features/practice-workspace/components/layout/ResizablePanels.tsx create mode 100644 codehive-frontend/app/features/practice-workspace/components/layout/WorkspaceHeader.tsx create mode 100644 codehive-frontend/app/features/practice-workspace/components/layout/WorkspaceLayout.tsx create mode 100644 codehive-frontend/app/features/practice-workspace/components/problem/ConstraintsSection.tsx create mode 100644 codehive-frontend/app/features/practice-workspace/components/problem/ExamplesSection.tsx create mode 100644 codehive-frontend/app/features/practice-workspace/components/problem/ProblemDescription.tsx create mode 100644 codehive-frontend/app/features/practice-workspace/components/problem/TagsSection.tsx create mode 100644 codehive-frontend/app/features/practice-workspace/components/submission/RunButton.tsx create mode 100644 codehive-frontend/app/features/practice-workspace/components/submission/SubmitButton.tsx create mode 100644 codehive-frontend/app/features/practice-workspace/components/testcase/AddTestCaseButton.tsx create mode 100644 codehive-frontend/app/features/practice-workspace/components/testcase/TestCaseEditor.tsx create mode 100644 codehive-frontend/app/features/practice-workspace/components/testcase/TestCasePanel.tsx create mode 100644 codehive-frontend/app/features/practice-workspace/components/testcase/TestCaseTabs.tsx create mode 100644 codehive-frontend/app/features/practice-workspace/data.ts create mode 100644 codehive-frontend/app/features/practice-workspace/hooks/useDraftPersistence.ts create mode 100644 codehive-frontend/app/features/practice-workspace/hooks/useExecution.ts create mode 100644 codehive-frontend/app/features/practice-workspace/hooks/useMonacoConfig.ts create mode 100644 codehive-frontend/app/features/practice-workspace/pages/PracticeWorkspacePage.tsx create mode 100644 codehive-frontend/app/features/practice-workspace/routes.ts create mode 100644 codehive-frontend/app/features/practice-workspace/routes/practice-workspace.tsx create mode 100644 codehive-frontend/app/features/practice-workspace/services/draft.service.ts create mode 100644 codehive-frontend/app/features/practice-workspace/services/execution.service.ts create mode 100644 codehive-frontend/app/features/practice-workspace/store/workspace.store.ts create mode 100644 codehive-frontend/app/features/practice-workspace/types/draft.types.ts create mode 100644 codehive-frontend/app/features/practice-workspace/types/editor.types.ts create mode 100644 codehive-frontend/app/features/practice-workspace/types/execution.types.ts create mode 100644 codehive-frontend/app/features/practice-workspace/types/testcase.types.ts create mode 100644 codehive-frontend/app/shared/components/ui/Badge.tsx create mode 100644 codehive-frontend/app/shared/components/ui/Button.tsx create mode 100644 codehive-frontend/app/shared/components/ui/Select.tsx create mode 100644 codehive-frontend/app/shared/components/ui/Separator.tsx create mode 100644 codehive-frontend/app/shared/components/ui/Tabs.tsx create mode 100644 codehive-frontend/app/shared/components/ui/Textarea.tsx create mode 100644 codehive-frontend/app/shared/lib/utils.ts diff --git a/codehive-frontend/app/core/router/routes.ts b/codehive-frontend/app/core/router/routes.ts index cabf068..bd96861 100644 --- a/codehive-frontend/app/core/router/routes.ts +++ b/codehive-frontend/app/core/router/routes.ts @@ -3,10 +3,12 @@ import { authRoutes } from "../../features/auth/routes"; import { adminRoutes } from "../../features/admin/routes"; import { landingRoutes } from "../../features/landing/routes"; import { dashboardRoutes } from "../../features/dashboard/routes"; +import { practiceWorkspaceRoutes } from "../../features/practice-workspace/routes"; export const routes = [ ...landingRoutes, ...authRoutes, ...adminRoutes, ...dashboardRoutes, + ...practiceWorkspaceRoutes, ] satisfies RouteConfig; diff --git a/codehive-frontend/app/features/auth/pages/LoginPage.tsx b/codehive-frontend/app/features/auth/pages/LoginPage.tsx index 99e2f36..c4841e1 100644 --- a/codehive-frontend/app/features/auth/pages/LoginPage.tsx +++ b/codehive-frontend/app/features/auth/pages/LoginPage.tsx @@ -37,198 +37,211 @@ export function LoginPage() { e.preventDefault(); setIsLoading(true); try { - const response = await AuthService.login({ - identifier, - password, - }); + const response = await AuthService.login({ identifier, password }); setAuthToken(response.data.token); await refreshUser(); sileo.success({ title: "Successfully logged in!" }); } catch (error: any) { console.error(error); - sileo.error({ title: error.message || "Failed to log in. Check your credentials." }); + sileo.error({ + title: error.message || "Failed to log in. Check your credentials.", + }); setIsLoading(false); } }; + const features = [ + "Access all your classrooms", + "Continue coding challenges", + "Track your progress", + ]; + return ( -
- {/* Left side - Branding/Illustration */} +
+ {/* ── Left panel ── */}
- {/* Background decorations */} -
-
-
-
- - {/* Grid pattern */} + {/* Subtle blobs */} +
+
+
+ {/* Grid */}
- {/* Content */} -
- {/* Logo */} - - CodeHive - CodeHive - - -

- Welcome back to -
- CodeHive -

+ {/* Logo – top of panel */} +
+ CodeHive + CodeHive + -

- Continue your coding journey. Access your classrooms, challenges, and - track your progress. -

+ {/* Content – vertically centered */} +
+ {/* Heading */} +
+

+ Welcome back to
+ CodeHive +

+

+ The ultimate engineering environment for academic excellence and + technical mastery. +

+
- {/* Features list */} -
- {[ - { text: "Access all your classrooms" }, - { text: "Continue coding challenges" }, - { text: "Track your progress" }, - ].map((item) => ( -
- {item.text} -
+ {/* Feature list */} +
    + {features.map((feature) => ( +
  • +
    + + + + +
    + + {feature} + +
  • ))} -
+ - {/* Decorative code block */} -
-
+ {/* Code block */} +
+
-
+
const student - = - {`{`} + = {"{"}
- name + name : - "You" + "You" ,
- status + status : - "Ready to learn" + "Ready to learn"
- {`}`}; + {"}"} + ;
+ + {/* Copyright */} +
+ © 2026 CodeHive. +
- {/* Right side - Login Form */} + {/* ── Right panel ── */}
- {/* Header with theme toggle */} -
- {/* Mobile logo */} - - CodeHive - CodeHive + {/* Top bar – theme toggle */} +
+ {/* Mobile logo (left panel hidden on mobile) */} + + CodeHive + CodeHive -
- - {/* Theme toggle */} - +
+ +
- {/* Form container */} -
-
- {/* Header */} -
-

+ {/* Form – centered vertically */} +
+
+ {/* Heading */} +
+

Sign in

-

- Sign in with your credentials +

+ New here?{" "} + + Create one + {" "} + account.

-
+ - {/* Divider */} -
-
-
-
-
- - Continue with your credentials - -
-
- - {/* Login form */} + {/* Form fields */}
- {/* Email or Enrollment Number field */} -
+ {/* Email / Identifier */} +
-
+
setIdentifier(e.target.value)} - placeholder="you@example.com or 2024123456" + placeholder="name@university.edu" required - className="w-full pl-12 pr-4 py-3 rounded-xl border border-gray-200 dark:border-gray-700 - bg-white dark:bg-dark-card text-gray-900 dark:text-white - placeholder:text-gray-400 dark:placeholder:text-gray-500 - focus:outline-none focus:ring-2 focus:ring-azure dark:focus:ring-yellow + className="w-full pl-12 pr-4 py-3.5 rounded-lg + border border-gray-200 dark:border-white/10 + bg-white dark:bg-white/5 + text-gray-900 dark:text-white text-base + placeholder:text-gray-400 dark:placeholder:text-gray-600 + focus:outline-none focus:ring-2 focus:ring-azure dark:focus:ring-[#6ba3f5] focus:border-transparent transition-all duration-200" />
- {/* Password field */} -
- -
+ {/* Password */} +
+
+ + + Forgot password? + +
+
setPassword(e.target.value)} placeholder="••••••••" required - className="w-full pl-12 pr-12 py-3 rounded-xl border border-gray-200 dark:border-gray-700 - bg-white dark:bg-dark-card text-gray-900 dark:text-white - placeholder:text-gray-400 dark:placeholder:text-gray-500 - focus:outline-none focus:ring-2 focus:ring-azure dark:focus:ring-yellow + className="w-full pl-12 pr-12 py-3.5 rounded-lg + border border-gray-200 dark:border-white/10 + bg-white dark:bg-white/5 + text-gray-900 dark:text-white text-base + placeholder:text-gray-400 dark:placeholder:text-gray-600 + focus:outline-none focus:ring-2 focus:ring-azure dark:focus:ring-[#6ba3f5] focus:border-transparent transition-all duration-200" />
- {/* Remember me and Forgot password */} -
- - + setRememberMe(e.target.checked)} + className="w-4 h-4 rounded border-gray-300 dark:border-white/20 + text-azure focus:ring-azure bg-white dark:bg-white/5" + /> + + Remember me for 30 days +
- {/* Submit button */} + {/* Submit */} +
+
- {/* Footer */} -

- By signing in, you agree to our{" "} + {/* Footer – pinned to bottom */} +

+
diff --git a/codehive-frontend/app/features/auth/pages/RecoveryPasswordPage.tsx b/codehive-frontend/app/features/auth/pages/RecoveryPasswordPage.tsx index 8756e74..b8458ad 100644 --- a/codehive-frontend/app/features/auth/pages/RecoveryPasswordPage.tsx +++ b/codehive-frontend/app/features/auth/pages/RecoveryPasswordPage.tsx @@ -16,12 +16,8 @@ export function RecoveryPasswordPage() { setMounted(true); }, []); - // Check if identifier is an email - const isEmail = (value: string) => { - return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test( - value.trim(), - ); - }; + const isEmail = (value: string) => + /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value.trim()); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -29,9 +25,7 @@ export function RecoveryPasswordPage() { const usedEnrollmentNumber = !isEmail(identifier); setIsEnrollmentNumber(usedEnrollmentNumber); try { - await AuthService.forgotPassword({ - identifier, - }); + await AuthService.forgotPassword({ identifier }); setIsSubmitted(true); } catch (error: unknown) { const message = @@ -43,218 +37,217 @@ export function RecoveryPasswordPage() { setIsLoading(false); }; + const features = [ + "Secure password reset link", + "Link expires in 24 hours", + "One-time use only", + ]; + return ( -
- {/* Left side - Branding/Illustration */} +
+ {/* ── Left panel ── */}
- {/* Background decorations */} -
-
-
-
- - {/* Grid pattern */} + {/* Blobs */} +
+
+
- {/* Content */} -
- {/* Logo */} - - CodeHive - CodeHive - + {/* Logo – top */} + + CodeHive + CodeHive + -

- Forgot your -
- password? -

+ {/* Content – vertically centered */} +
+ {isSubmitted ? ( +
+ {/* Badge */} +
+ + Security Protocol +
-

- No worries! It happens to the best of us. Enter your email or - enrollment number and we'll send you instructions to reset your - password. -

+
+

+ Check your
+ email! +

+

+ We've dispatched an encrypted verification link to your registered inbox. Please complete the process to resume your engineering workspace. +

+
- {/* Security features */} -
- {[ - { text: "Secure password reset link" }, - { text: "Link expires in 24 hours" }, - { text: "One-time use only" }, - ].map((item) => ( -
-
- - - +
+
+
+ + + +
+
+

Link expires in 24 hours

+

For security reasons, this link will self-destruct after one day.

+
- {item.text} -
- ))} -
- {/* Decorative lock illustration */} -
-
-
-
- - - +
+
+ + + +
+
+

Check your spam folder

+

If it's not in your inbox, our carrier pigeons might have taken a detour.

+
-
- - - + +
+
+ + + +
+
+

One-time use only

+

The link is uniquely generated and can only be used to verify once.

+
-

- Your account security is our priority -

-
+ ) : ( +
+
+

+ Forgot your
+ password? +

+

+ No worries! It happens to the best of us. Enter your email and we'll + send you instructions to reset your password. +

+
+ +
    + {features.map((feature) => ( +
  • +
    + + + +
    + {feature} +
  • + ))} +
+ +
+
+
+
+ + + +
+ + + +
+
+
+
+
+ )}
+ + {/* Bottom text */} +

+ Your account security is our priority +

- {/* Right side - Recovery Form */} + {/* ── Right panel ── */}
- {/* Header with theme toggle */} -
- {/* Mobile logo */} - - CodeHive - CodeHive + {/* Top bar */} +
+ + CodeHive + CodeHive -
- - {/* Theme toggle */} - +
+ +
- {/* Form container */} -
-
+ {/* Form – centered vertically */} +
+
{!isSubmitted ? ( <> - {/* Header */} -
- {/* Icon */} -
+ {/* Icon + heading */} +
+
- +
- -

- Reset password -

-

- Enter the email address or enrollment number associated with - your account -

+
+

+ Reset password +

+

+ Enter the email address associated with your account +

+
- {/* Recovery form */} -
- {/* Email or Enrollment Number field */} -
+ {/* Form */} + +
-
+
@@ -272,44 +265,32 @@ export function RecoveryPasswordPage() { type="text" value={identifier} onChange={(e) => setIdentifier(e.target.value)} - placeholder="you@example.com or 2024123456" + placeholder="you@example.com" required - className="w-full pl-12 pr-4 py-3 rounded-xl border border-gray-200 dark:border-gray-700 - bg-white dark:bg-dark-card text-gray-900 dark:text-white - placeholder:text-gray-400 dark:placeholder:text-gray-500 - focus:outline-none focus:ring-2 focus:ring-azure dark:focus:ring-yellow + className="w-full pl-12 pr-4 py-3.5 rounded-lg + border border-gray-200 dark:border-white/10 + bg-white dark:bg-white/5 + text-gray-900 dark:text-white text-base + placeholder:text-gray-400 dark:placeholder:text-gray-600 + focus:outline-none focus:ring-2 focus:ring-azure dark:focus:ring-[#6ba3f5] focus:border-transparent transition-all duration-200" />
- {/* Submit button */} - {/* Back to login */} -
+ {/* Back to sign in */} + - - ) : ( - /* Success state */ -
- {/* Success icon */} -
- - - -
-

- Check your email -

-

- {isEnrollmentNumber - ? "We've sent password reset instructions to the email associated with enrollment number" - : "We've sent password reset instructions to"} -

-

- {identifier} -

- - {/* Email icon animation */} -
- - - + {/* Security tip */} +
+
+
+

Security tip:

+

+ The reset link will be sent to your registered email address + and will expire in{" "} + 24 hours for security purposes. +

+
+ + ) : ( + /* ── Success state ── */ +
+
+ {/* Header – centered */} +
+
+ + + +
+
+

Email sent!

+

+ {isEnrollmentNumber + ? "We've sent a reset link to the email associated with enrollment number" + : "We've sent a password reset link to:"} +

+ {identifier} +
+
-

- Didn't receive the email? Check your spam folder or -

- - {/* Resend button */} - + {/* Callout box */} +
+ + + +
+

Important Instruction

+

Ensure you use the link in the same browser session for optimal security synchronization.

+
+
- {/* Divider */} -
-
-
+ {/* Actions */} +
+ +
-
- - or - + + {/* Footer */} +
+

+ Having trouble?{" "} + + Contact Technical Support + +

- - {/* Back to login */} - - - - - Back to sign in -
)}
diff --git a/codehive-frontend/app/features/landing/components/Contact.tsx b/codehive-frontend/app/features/landing/components/Contact.tsx index 6275b74..d02780b 100644 --- a/codehive-frontend/app/features/landing/components/Contact.tsx +++ b/codehive-frontend/app/features/landing/components/Contact.tsx @@ -77,13 +77,16 @@ export function Contact() {
{[ - { text: "Academic & institutional pricing" }, - { text: "Custom integrations & features" }, - { text: "Dedicated support & SLA" }, - { text: "Scalable for any class size" }, - ].map((item) => ( -
- {item.text} + "Academic & institutional pricing", + "Custom integrations & features", + "Dedicated support & SLA", + "Scalable for any class size", + ].map((text) => ( +
+ + + + {text}
))}
diff --git a/codehive-frontend/app/features/landing/components/Footer.tsx b/codehive-frontend/app/features/landing/components/Footer.tsx index 619bf9f..64bc8e6 100644 --- a/codehive-frontend/app/features/landing/components/Footer.tsx +++ b/codehive-frontend/app/features/landing/components/Footer.tsx @@ -2,28 +2,19 @@ import logo from "~/assets/logo.png"; const footerLinks = { - Product: [ + Platform: [ { name: "Features", href: "#features" }, - { name: "Pricing", href: "#pricing" }, - { name: "Integrations", href: "#" }, - { name: "Changelog", href: "#" }, + { name: "How It Works", href: "#how-it-works" }, + { name: "Meet the Team", href: "#creators" }, + { name: "Contact", href: "#contact" }, ], - Resources: [ - { name: "Documentation", href: "#" }, - { name: "Tutorials", href: "#" }, - { name: "Blog", href: "#" }, - { name: "Community", href: "#" }, - ], - Company: [ - { name: "About", href: "#" }, - { name: "Careers", href: "#" }, - { name: "Contact", href: "#" }, - { name: "Press Kit", href: "#" }, + Account: [ + { name: "Sign In", href: "/login" }, + { name: "Forgot Password", href: "/forgot-password" }, ], Legal: [ - { name: "Privacy", href: "#" }, - { name: "Terms", href: "#" }, - { name: "Security", href: "#" }, + { name: "Privacy Policy", href: "#" }, + { name: "Terms of Service", href: "#" }, ], }; @@ -87,7 +78,7 @@ export function Footer() { Ready to transform your classroom?

- Join thousands of educators who are already using CodeHive to teach programming more effectively. + Be among the first to experience a smarter way to teach programming — built for universities, designed for students.

diff --git a/codehive-frontend/app/features/landing/components/Hero.tsx b/codehive-frontend/app/features/landing/components/Hero.tsx index e820e23..8d866d4 100644 --- a/codehive-frontend/app/features/landing/components/Hero.tsx +++ b/codehive-frontend/app/features/landing/components/Hero.tsx @@ -155,12 +155,12 @@ export function Hero() {
- +
-
Team Collab
-
Real-time
+
Secure Sandbox
+
Isolated execution
diff --git a/codehive-frontend/app/features/landing/components/HowItWorks.tsx b/codehive-frontend/app/features/landing/components/HowItWorks.tsx index 0031232..8845f4f 100644 --- a/codehive-frontend/app/features/landing/components/HowItWorks.tsx +++ b/codehive-frontend/app/features/landing/components/HowItWorks.tsx @@ -12,7 +12,7 @@ const steps = [ { number: "02", title: "Design Challenges", - description: "Create coding problems with test cases, time limits, and hints. Support for 20+ languages.", + description: "Create coding problems with test cases, time limits, and hints. Support for multiple programming languages.", icon: ( @@ -117,7 +117,7 @@ export function HowItWorks() { {/* CTA */}
- + Get Started Now diff --git a/codehive-frontend/app/features/practice-workspace/api/execution.api.ts b/codehive-frontend/app/features/practice-workspace/api/execution.api.ts new file mode 100644 index 0000000..1ce257c --- /dev/null +++ b/codehive-frontend/app/features/practice-workspace/api/execution.api.ts @@ -0,0 +1,5 @@ +import type { ExecutionRequest } from '../types/execution.types' + +export async function executeCode(_request: ExecutionRequest) { + return Promise.resolve() +} diff --git a/codehive-frontend/app/features/practice-workspace/components/editor/CodeEditor.tsx b/codehive-frontend/app/features/practice-workspace/components/editor/CodeEditor.tsx new file mode 100644 index 0000000..eb0543f --- /dev/null +++ b/codehive-frontend/app/features/practice-workspace/components/editor/CodeEditor.tsx @@ -0,0 +1,28 @@ +import Editor from '@monaco-editor/react' + +import { useMonacoConfig } from '../../hooks/useMonacoConfig' +import { useWorkspaceStore } from '../../store/workspace.store' +import { EditorToolbar } from './EditorToolbar' + +export function CodeEditor() { + const code = useWorkspaceStore((state) => state.code) + const setCode = useWorkspaceStore((state) => state.setCode) + + const { options, monacoLanguage } = useMonacoConfig() + + return ( +
+ +
+ setCode(value ?? '')} + /> +
+ + ) +} diff --git a/codehive-frontend/app/features/practice-workspace/components/editor/EditorToolbar.tsx b/codehive-frontend/app/features/practice-workspace/components/editor/EditorToolbar.tsx new file mode 100644 index 0000000..23428d7 --- /dev/null +++ b/codehive-frontend/app/features/practice-workspace/components/editor/EditorToolbar.tsx @@ -0,0 +1,42 @@ +import { Maximize2, Minimize2 } from 'lucide-react' + +import { Button } from '~/shared/components/ui/Button' +import { useExecution } from '../../hooks/useExecution' +import { useWorkspaceStore } from '../../store/workspace.store' +import { RunButton } from '../submission/RunButton' +import { SubmitButton } from '../submission/SubmitButton' +import { LanguageSelector } from './LanguageSelector' + +export function EditorToolbar() { + const { run, submit } = useExecution() + const isEditorExpanded = useWorkspaceStore((state) => state.isEditorExpanded) + const toggleEditorExpanded = useWorkspaceStore( + (state) => state.toggleEditorExpanded, + ) + + return ( +
+
+ +
+ +
+ + + +
+
+ ) +} diff --git a/codehive-frontend/app/features/practice-workspace/components/editor/LanguageSelector.tsx b/codehive-frontend/app/features/practice-workspace/components/editor/LanguageSelector.tsx new file mode 100644 index 0000000..2dea841 --- /dev/null +++ b/codehive-frontend/app/features/practice-workspace/components/editor/LanguageSelector.tsx @@ -0,0 +1,36 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '~/shared/components/ui/Select' + +import { allowedLanguages, languageLabels } from '../../data' +import { useWorkspaceStore } from '../../store/workspace.store' +import { Language } from '../../types/execution.types' + +export function LanguageSelector() { + const language = useWorkspaceStore((state) => state.language) + const setLanguage = useWorkspaceStore((state) => state.setLanguage) + + return ( +
+ +
+ ) +} diff --git a/codehive-frontend/app/features/practice-workspace/components/layout/ResizablePanels.tsx b/codehive-frontend/app/features/practice-workspace/components/layout/ResizablePanels.tsx new file mode 100644 index 0000000..fa315c0 --- /dev/null +++ b/codehive-frontend/app/features/practice-workspace/components/layout/ResizablePanels.tsx @@ -0,0 +1,66 @@ +import type { ReactNode } from 'react' +import { + Group as PanelGroup, + Panel, + Separator as PanelResizeHandle, +} from 'react-resizable-panels' + +import { cn } from '~/shared/lib/utils' +import { useWorkspaceStore } from '../../store/workspace.store' + +type ResizablePanelsProps = { + left: ReactNode + right: ReactNode + bottom: ReactNode +} + +function ResizeHandle({ direction }: { direction: 'horizontal' | 'vertical' }) { + return ( + + ) +} + +export function ResizablePanels({ left, right, bottom }: ResizablePanelsProps) { + const isEditorExpanded = useWorkspaceStore((state) => state.isEditorExpanded) + + if (isEditorExpanded) { + return ( + + + {right} + + + + {bottom} + + + ) + } + + return ( + + + {left} + + + + + + {right} + + + + {bottom} + + + + + ) +} diff --git a/codehive-frontend/app/features/practice-workspace/components/layout/WorkspaceHeader.tsx b/codehive-frontend/app/features/practice-workspace/components/layout/WorkspaceHeader.tsx new file mode 100644 index 0000000..eb5230a --- /dev/null +++ b/codehive-frontend/app/features/practice-workspace/components/layout/WorkspaceHeader.tsx @@ -0,0 +1,23 @@ +import { Link } from 'react-router' +import { Code2, ArrowLeft } from 'lucide-react' + +export function WorkspaceHeader() { + return ( +
+
+ + + Back + +
+
+ + CodeHive +
+
+
+ ) +} diff --git a/codehive-frontend/app/features/practice-workspace/components/layout/WorkspaceLayout.tsx b/codehive-frontend/app/features/practice-workspace/components/layout/WorkspaceLayout.tsx new file mode 100644 index 0000000..191a17f --- /dev/null +++ b/codehive-frontend/app/features/practice-workspace/components/layout/WorkspaceLayout.tsx @@ -0,0 +1,25 @@ +import * as React from 'react' + +import { ResizablePanels } from './ResizablePanels' +import { WorkspaceHeader } from './WorkspaceHeader' + +type WorkspaceLayoutProps = { + children: React.ReactNode +} + +export function WorkspaceLayout({ children }: WorkspaceLayoutProps) { + const panels = React.Children.toArray(children) + + return ( +
+ +
+ +
+
+ ) +} diff --git a/codehive-frontend/app/features/practice-workspace/components/problem/ConstraintsSection.tsx b/codehive-frontend/app/features/practice-workspace/components/problem/ConstraintsSection.tsx new file mode 100644 index 0000000..9d656b5 --- /dev/null +++ b/codehive-frontend/app/features/practice-workspace/components/problem/ConstraintsSection.tsx @@ -0,0 +1,22 @@ +import { Separator } from '~/shared/components/ui/Separator' + +type ConstraintsSectionProps = { + constraints: string[] +} + +export function ConstraintsSection({ constraints }: ConstraintsSectionProps) { + return ( +
+
+

Constraints

+ +
+ +
    + {constraints.map((constraint) => ( +
  • {constraint}
  • + ))} +
+
+ ) +} diff --git a/codehive-frontend/app/features/practice-workspace/components/problem/ExamplesSection.tsx b/codehive-frontend/app/features/practice-workspace/components/problem/ExamplesSection.tsx new file mode 100644 index 0000000..224dfa5 --- /dev/null +++ b/codehive-frontend/app/features/practice-workspace/components/problem/ExamplesSection.tsx @@ -0,0 +1,38 @@ +import { Separator } from '~/shared/components/ui/Separator' + +type ExampleItem = { + title: string + input: string + output: string +} + +type ExamplesSectionProps = { + examples: ExampleItem[] +} + +export function ExamplesSection({ examples }: ExamplesSectionProps) { + return ( +
+
+

Examples

+ +
+ +
+ {examples.map((example) => ( +
+

{example.title}

+
+

+ Input: {example.input} +

+

+ Output: {example.output} +

+
+
+ ))} +
+
+ ) +} diff --git a/codehive-frontend/app/features/practice-workspace/components/problem/ProblemDescription.tsx b/codehive-frontend/app/features/practice-workspace/components/problem/ProblemDescription.tsx new file mode 100644 index 0000000..08d16c9 --- /dev/null +++ b/codehive-frontend/app/features/practice-workspace/components/problem/ProblemDescription.tsx @@ -0,0 +1,39 @@ +import { Separator } from '~/shared/components/ui/Separator' + +import { mockProblem } from '../../data' +import { ConstraintsSection } from './ConstraintsSection' +import { ExamplesSection } from './ExamplesSection' +import { TagsSection } from './TagsSection' + +export function ProblemDescription() { + return ( + + ) +} diff --git a/codehive-frontend/app/features/practice-workspace/components/problem/TagsSection.tsx b/codehive-frontend/app/features/practice-workspace/components/problem/TagsSection.tsx new file mode 100644 index 0000000..751e48d --- /dev/null +++ b/codehive-frontend/app/features/practice-workspace/components/problem/TagsSection.tsx @@ -0,0 +1,25 @@ +import { Badge } from '~/shared/components/ui/Badge' +import { Separator } from '~/shared/components/ui/Separator' + +type TagsSectionProps = { + tags: string[] +} + +export function TagsSection({ tags }: TagsSectionProps) { + return ( +
+
+

Tags

+ +
+ +
+ {tags.map((tag) => ( + + {tag} + + ))} +
+
+ ) +} diff --git a/codehive-frontend/app/features/practice-workspace/components/submission/RunButton.tsx b/codehive-frontend/app/features/practice-workspace/components/submission/RunButton.tsx new file mode 100644 index 0000000..c172221 --- /dev/null +++ b/codehive-frontend/app/features/practice-workspace/components/submission/RunButton.tsx @@ -0,0 +1,16 @@ +import { Play } from 'lucide-react' + +import { Button } from '~/shared/components/ui/Button' + +type RunButtonProps = { + onRun: () => void +} + +export function RunButton({ onRun }: RunButtonProps) { + return ( + + ) +} diff --git a/codehive-frontend/app/features/practice-workspace/components/submission/SubmitButton.tsx b/codehive-frontend/app/features/practice-workspace/components/submission/SubmitButton.tsx new file mode 100644 index 0000000..faef55f --- /dev/null +++ b/codehive-frontend/app/features/practice-workspace/components/submission/SubmitButton.tsx @@ -0,0 +1,16 @@ +import { Send } from 'lucide-react' + +import { Button } from '~/shared/components/ui/Button' + +type SubmitButtonProps = { + onSubmit: () => void +} + +export function SubmitButton({ onSubmit }: SubmitButtonProps) { + return ( + + ) +} diff --git a/codehive-frontend/app/features/practice-workspace/components/testcase/AddTestCaseButton.tsx b/codehive-frontend/app/features/practice-workspace/components/testcase/AddTestCaseButton.tsx new file mode 100644 index 0000000..813e28d --- /dev/null +++ b/codehive-frontend/app/features/practice-workspace/components/testcase/AddTestCaseButton.tsx @@ -0,0 +1,16 @@ +import { Plus } from 'lucide-react' + +import { Button } from '~/shared/components/ui/Button' + +type AddTestCaseButtonProps = { + onAdd: () => void +} + +export function AddTestCaseButton({ onAdd }: AddTestCaseButtonProps) { + return ( + + ) +} diff --git a/codehive-frontend/app/features/practice-workspace/components/testcase/TestCaseEditor.tsx b/codehive-frontend/app/features/practice-workspace/components/testcase/TestCaseEditor.tsx new file mode 100644 index 0000000..eea75a5 --- /dev/null +++ b/codehive-frontend/app/features/practice-workspace/components/testcase/TestCaseEditor.tsx @@ -0,0 +1,29 @@ +import { Textarea } from '~/shared/components/ui/Textarea' + +import { useWorkspaceStore } from '../../store/workspace.store' + +export function TestCaseEditor() { + const selectedTestCase = useWorkspaceStore((state) => state.selectedTestCase) + const testCases = useWorkspaceStore((state) => state.testCases) + const updateTestCase = useWorkspaceStore((state) => state.updateTestCase) + + const value = testCases[selectedTestCase] ?? '' + + return ( +
+
+

Input

+

Case {selectedTestCase + 1}

+
+ +