remove k8s configs, update worker for multi-languages support, add local-submit option
This commit is contained in:
256
README.md
256
README.md
@@ -13,10 +13,11 @@
|
|||||||
- Скачивание пакетов задач из Polygon
|
- Скачивание пакетов задач из Polygon
|
||||||
- Маршрутизация запросов к соответствующим Worker'ам по языку программирования
|
- Маршрутизация запросов к соответствующим Worker'ам по языку программирования
|
||||||
|
|
||||||
### 2. Worker (Тестировщик)
|
### 2. Worker (Универсальный тестировщик)
|
||||||
- **Технология**: ASP.NET Core Web API + компиляторы языков
|
- **Технология**: ASP.NET Core Web API + компиляторы всех поддерживаемых языков
|
||||||
- **Функции**:
|
- **Функции**:
|
||||||
- Компиляция пользовательского кода
|
- Динамический выбор компилятора/интерпретатора на основе языка
|
||||||
|
- Компиляция пользовательского кода (C++, Java, Kotlin, C#) или подготовка к исполнению (Python)
|
||||||
- Запуск в изолированной среде
|
- Запуск в изолированной среде
|
||||||
- Тестирование на наборе тестов
|
- Тестирование на наборе тестов
|
||||||
- Контроль ограничений по времени и памяти
|
- Контроль ограничений по времени и памяти
|
||||||
@@ -28,10 +29,11 @@
|
|||||||
|
|
||||||
## Поддерживаемые языки
|
## Поддерживаемые языки
|
||||||
|
|
||||||
- **C++** (текущая реализация)
|
- **C++** (реализовано)
|
||||||
- Java (планируется)
|
- **Java** (реализовано)
|
||||||
- Kotlin (планируется)
|
- **Kotlin** (реализовано)
|
||||||
- C# (планируется)
|
- **C#** (реализовано)
|
||||||
|
- **Python** (реализовано)
|
||||||
|
|
||||||
## Модель данных
|
## Модель данных
|
||||||
|
|
||||||
@@ -87,11 +89,71 @@ cd src/LiquidCode.Tester.Worker
|
|||||||
dotnet run
|
dotnet run
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Локальное тестирование
|
||||||
|
|
||||||
|
Для тестирования системы без внешних зависимостей используйте endpoint `/api/Tester/submit-local`:
|
||||||
|
|
||||||
|
### 1. Подготовка тестового пакета
|
||||||
|
|
||||||
|
Создайте ZIP файл с тестами в формате:
|
||||||
|
```
|
||||||
|
test-package.zip
|
||||||
|
├── 1.in # Входные данные для теста 1
|
||||||
|
├── 1.out # Ожидаемый результат для теста 1
|
||||||
|
├── 2.in # Входные данные для теста 2
|
||||||
|
├── 2.out # Ожидаемый результат для теста 2
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Отправка запроса
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X 'POST' \
|
||||||
|
'http://localhost:8080/api/Tester/submit-local' \
|
||||||
|
-F 'id=1' \
|
||||||
|
-F 'missionId=1' \
|
||||||
|
-F 'language=python' \
|
||||||
|
-F 'languageVersion=3.11' \
|
||||||
|
-F 'sourceCode=n = int(input())
|
||||||
|
print(n * 2)' \
|
||||||
|
-F 'callbackUrl=log' \
|
||||||
|
-F 'package=@./test-package.zip'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Просмотр результатов
|
||||||
|
|
||||||
|
При использовании `callbackUrl=log` результаты тестирования выводятся в консоль Worker:
|
||||||
|
|
||||||
|
```
|
||||||
|
╔═══════════════════════════════════════════════════════════════╗
|
||||||
|
║ CALLBACK RESULT ║
|
||||||
|
╠═══════════════════════════════════════════════════════════════╣
|
||||||
|
{
|
||||||
|
"submitId": 1,
|
||||||
|
"state": "Done",
|
||||||
|
"errorCode": "None",
|
||||||
|
"message": "All tests passed",
|
||||||
|
"currentTest": 3,
|
||||||
|
"amountOfTests": 3
|
||||||
|
}
|
||||||
|
╚═══════════════════════════════════════════════════════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
|
### Специальные значения callbackUrl
|
||||||
|
|
||||||
|
- `"log"` - Вывод результатов в логи Worker
|
||||||
|
- `"console"` - Аналогично "log"
|
||||||
|
- `"log://"` - Аналогично "log"
|
||||||
|
- Любой HTTP URL - Отправка результатов на указанный endpoint
|
||||||
|
|
||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
|
||||||
### Gateway
|
### Gateway
|
||||||
|
|
||||||
**POST /api/tester/submit**
|
**POST /api/tester/submit**
|
||||||
|
|
||||||
|
Отправка решения с удаленным пакетом тестов:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id": 123,
|
"id": 123,
|
||||||
@@ -104,6 +166,31 @@ dotnet run
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**POST /api/tester/submit-local**
|
||||||
|
|
||||||
|
Отправка решения с локальным пакетом тестов (для отладки):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X 'POST' \
|
||||||
|
'http://localhost:8080/api/Tester/submit-local' \
|
||||||
|
-F 'id=1' \
|
||||||
|
-F 'missionId=1' \
|
||||||
|
-F 'language=python' \
|
||||||
|
-F 'languageVersion=3.11' \
|
||||||
|
-F 'sourceCode=print("Hello, World!")' \
|
||||||
|
-F 'callbackUrl=log' \
|
||||||
|
-F 'package=@/path/to/test-package.zip'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Параметры:**
|
||||||
|
- `id` - ID сабмита
|
||||||
|
- `missionId` - ID задачи
|
||||||
|
- `language` - Язык программирования (C++, Java, Kotlin, C#, Python)
|
||||||
|
- `languageVersion` - Версия языка (опционально, используется "latest" по умолчанию)
|
||||||
|
- `sourceCode` - Исходный код решения
|
||||||
|
- `callbackUrl` - URL для отправки результатов или `"log"` для вывода в консоль
|
||||||
|
- `package` - ZIP файл с тестами
|
||||||
|
|
||||||
**GET /api/tester/health**
|
**GET /api/tester/health**
|
||||||
- Проверка состояния Gateway
|
- Проверка состояния Gateway
|
||||||
|
|
||||||
@@ -121,7 +208,7 @@ dotnet run
|
|||||||
2. **Скачивание пакета**: Gateway скачивает Polygon пакет
|
2. **Скачивание пакета**: Gateway скачивает Polygon пакет
|
||||||
3. **Маршрутизация**: Gateway отправляет запрос в Worker для нужного языка
|
3. **Маршрутизация**: Gateway отправляет запрос в Worker для нужного языка
|
||||||
4. **Парсинг**: Worker распаковывает и парсит пакет (тесты, лимиты)
|
4. **Парсинг**: Worker распаковывает и парсит пакет (тесты, лимиты)
|
||||||
5. **Компиляция**: Worker компилирует код (g++ для C++)
|
5. **Компиляция**: Worker выбирает нужный компилятор на основе языка и компилирует код
|
||||||
6. **Тестирование**: Worker последовательно запускает все тесты
|
6. **Тестирование**: Worker последовательно запускает все тесты
|
||||||
7. **Callback**: Worker отправляет статусы на callback URL на каждом этапе
|
7. **Callback**: Worker отправляет статусы на callback URL на каждом этапе
|
||||||
8. **Cleanup**: Worker удаляет временные файлы
|
8. **Cleanup**: Worker удаляет временные файлы
|
||||||
@@ -141,17 +228,6 @@ dotnet run
|
|||||||
- chroot окружение
|
- chroot окружение
|
||||||
- Отключение доступа к сети для тестируемого кода
|
- Отключение доступа к сети для тестируемого кода
|
||||||
|
|
||||||
## Деплой в Kubernetes
|
|
||||||
|
|
||||||
См. [k8s/README.md](k8s/README.md) для подробной инструкции.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Быстрый деплой
|
|
||||||
kubectl apply -f k8s/namespace.yaml
|
|
||||||
kubectl apply -f k8s/configmap.yaml
|
|
||||||
kubectl apply -f k8s/worker-cpp-deployment.yaml
|
|
||||||
kubectl apply -f k8s/gateway-deployment.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
## Конфигурация
|
## Конфигурация
|
||||||
|
|
||||||
@@ -160,7 +236,11 @@ kubectl apply -f k8s/gateway-deployment.yaml
|
|||||||
{
|
{
|
||||||
"PackageDownloadDirectory": "/tmp/packages",
|
"PackageDownloadDirectory": "/tmp/packages",
|
||||||
"Workers": {
|
"Workers": {
|
||||||
"Cpp": "http://liquidcode-tester-worker-cpp:8080"
|
"Cpp": "http://localhost:8081",
|
||||||
|
"Java": "http://localhost:8081",
|
||||||
|
"Kotlin": "http://localhost:8081",
|
||||||
|
"CSharp": "http://localhost:8081",
|
||||||
|
"Python": "http://localhost:8081"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -169,10 +249,114 @@ kubectl apply -f k8s/gateway-deployment.yaml
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"Cpp": {
|
"Cpp": {
|
||||||
|
"Compiler": "g++",
|
||||||
|
"CompilerFlags": "-O2 -std=c++17 -Wall",
|
||||||
|
"Versions": {
|
||||||
|
"14": {
|
||||||
|
"Compiler": "g++",
|
||||||
|
"CompilerFlags": "-O2 -std=c++14 -Wall"
|
||||||
|
},
|
||||||
|
"17": {
|
||||||
"Compiler": "g++",
|
"Compiler": "g++",
|
||||||
"CompilerFlags": "-O2 -std=c++17 -Wall"
|
"CompilerFlags": "-O2 -std=c++17 -Wall"
|
||||||
|
},
|
||||||
|
"20": {
|
||||||
|
"Compiler": "g++",
|
||||||
|
"CompilerFlags": "-O2 -std=c++20 -Wall"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Java": {
|
||||||
|
"Compiler": "javac",
|
||||||
|
"CompilerFlags": "",
|
||||||
|
"Versions": {
|
||||||
|
"8": {
|
||||||
|
"Compiler": "javac",
|
||||||
|
"CompilerFlags": "-source 8 -target 8"
|
||||||
|
},
|
||||||
|
"11": {
|
||||||
|
"Compiler": "javac",
|
||||||
|
"CompilerFlags": "-source 11 -target 11"
|
||||||
|
},
|
||||||
|
"17": {
|
||||||
|
"Compiler": "javac",
|
||||||
|
"CompilerFlags": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Kotlin": {
|
||||||
|
"Compiler": "kotlinc",
|
||||||
|
"CompilerFlags": "",
|
||||||
|
"Versions": {
|
||||||
|
"1.9": {
|
||||||
|
"Compiler": "kotlinc",
|
||||||
|
"CompilerFlags": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CSharp": {
|
||||||
|
"Compiler": "csc",
|
||||||
|
"CompilerFlags": "/optimize+",
|
||||||
|
"Versions": {
|
||||||
|
"7": {
|
||||||
|
"Compiler": "csc",
|
||||||
|
"CompilerFlags": "/optimize+ /langversion:7"
|
||||||
|
},
|
||||||
|
"8": {
|
||||||
|
"Compiler": "csc",
|
||||||
|
"CompilerFlags": "/optimize+ /langversion:8"
|
||||||
|
},
|
||||||
|
"9": {
|
||||||
|
"Compiler": "csc",
|
||||||
|
"CompilerFlags": "/optimize+ /langversion:9"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Python": {
|
||||||
|
"Executable": "python3",
|
||||||
|
"Versions": {
|
||||||
|
"3.8": {
|
||||||
|
"Executable": "python3.8"
|
||||||
|
},
|
||||||
|
"3.9": {
|
||||||
|
"Executable": "python3.9"
|
||||||
|
},
|
||||||
|
"3.10": {
|
||||||
|
"Executable": "python3.10"
|
||||||
|
},
|
||||||
|
"3.11": {
|
||||||
|
"Executable": "python3.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Поддержка версий языков
|
||||||
|
|
||||||
|
Система поддерживает указание версии языка программирования через параметр `languageVersion`:
|
||||||
|
|
||||||
|
- **"latest"** или **null** - использует версию по умолчанию из основной конфигурации
|
||||||
|
- **Конкретная версия** (например, "17", "3.11") - использует конфигурацию из секции `Versions`
|
||||||
|
- **Несуществующая версия** - логируется предупреждение и используется версия по умолчанию
|
||||||
|
|
||||||
|
Каждый язык имеет свою конфигурацию с возможностью указания:
|
||||||
|
- Для компилируемых языков (C++, Java, Kotlin, C#): путь к компилятору и флаги компиляции
|
||||||
|
- Для интерпретируемых языков (Python): путь к интерпретатору
|
||||||
|
|
||||||
|
Пример использования:
|
||||||
|
```bash
|
||||||
|
# Использование Python 3.11
|
||||||
|
curl -X POST 'http://localhost:8080/api/Tester/submit-local' \
|
||||||
|
-F 'languageVersion=3.11' \
|
||||||
|
-F 'language=python' \
|
||||||
|
...
|
||||||
|
|
||||||
|
# Использование C++20
|
||||||
|
curl -X POST 'http://localhost:8080/api/Tester/submit-local' \
|
||||||
|
-F 'languageVersion=20' \
|
||||||
|
-F 'language=C++' \
|
||||||
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
## Структура проекта
|
## Структура проекта
|
||||||
@@ -186,11 +370,13 @@ LiquidCode.Tester/
|
|||||||
│ │ ├── Controllers/
|
│ │ ├── Controllers/
|
||||||
│ │ ├── Services/
|
│ │ ├── Services/
|
||||||
│ │ └── Dockerfile
|
│ │ └── Dockerfile
|
||||||
│ └── LiquidCode.Tester.Worker/ # C++ Worker
|
│ └── LiquidCode.Tester.Worker/ # Универсальный Worker (все языки)
|
||||||
│ ├── Controllers/
|
│ ├── Controllers/
|
||||||
│ ├── Services/
|
│ ├── Services/
|
||||||
|
│ │ ├── Compilation/ # Компиляторы
|
||||||
|
│ │ ├── Execution/ # Исполнители
|
||||||
|
│ │ └── Factories/ # Фабрики
|
||||||
│ └── Dockerfile
|
│ └── Dockerfile
|
||||||
├── k8s/ # Kubernetes манифесты
|
|
||||||
├── compose.yaml # Docker Compose
|
├── compose.yaml # Docker Compose
|
||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
@@ -199,19 +385,29 @@ LiquidCode.Tester/
|
|||||||
|
|
||||||
- .NET 9.0 SDK
|
- .NET 9.0 SDK
|
||||||
- Docker & Docker Compose (для контейнеризации)
|
- Docker & Docker Compose (для контейнеризации)
|
||||||
- g++ (для C++ Worker)
|
- Компиляторы языков программирования (устанавливаются автоматически в Docker):
|
||||||
- Kubernetes cluster (для production)
|
- g++ (C++)
|
||||||
|
- OpenJDK 17 (Java)
|
||||||
|
- Kotlin compiler (Kotlin)
|
||||||
|
- Mono (C#)
|
||||||
|
- Python 3
|
||||||
|
|
||||||
## Расширение на другие языки
|
## Расширение на другие языки
|
||||||
|
|
||||||
Для добавления поддержки нового языка:
|
Для добавления поддержки нового языка:
|
||||||
|
|
||||||
1. Создайте новый Worker проект (или расширьте существующий)
|
1. Создайте новую реализацию `ICompilationService` для языка (например, `GoCompilationService.cs`)
|
||||||
2. Реализуйте `ICompilationService` для языка
|
2. Создайте новую реализацию `IExecutionService` для языка (например, `GoExecutionService.cs`)
|
||||||
3. Реализуйте `IExecutionService` для языка
|
3. Зарегистрируйте новые сервисы в `Program.cs`:
|
||||||
4. Обновите конфигурацию Gateway
|
```csharp
|
||||||
5. Создайте Dockerfile с нужным компилятором/runtime
|
builder.Services.AddSingleton<GoCompilationService>();
|
||||||
6. Добавьте Kubernetes манифесты
|
builder.Services.AddSingleton<GoExecutionService>();
|
||||||
|
```
|
||||||
|
4. Обновите фабрики (`CompilationServiceFactory` и `ExecutionServiceFactory`), добавив поддержку нового языка
|
||||||
|
5. Добавьте конфигурацию языка в `appsettings.json` Worker
|
||||||
|
6. Обновите `Dockerfile` Worker, добавив установку компилятора/runtime
|
||||||
|
7. Добавьте конфигурацию воркера для нового языка в `appsettings.json` Gateway
|
||||||
|
8. Обновите `WorkerClientService` Gateway, добавив маппинг языка на URL воркера
|
||||||
|
|
||||||
## Разработка
|
## Разработка
|
||||||
|
|
||||||
|
|||||||
14
compose.yaml
14
compose.yaml
@@ -9,15 +9,19 @@
|
|||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
environment:
|
environment:
|
||||||
- ASPNETCORE_ENVIRONMENT=Development
|
- ASPNETCORE_ENVIRONMENT=Development
|
||||||
- Workers__Cpp=http://worker-cpp:8080
|
- Workers__Cpp=http://worker:8080
|
||||||
|
- Workers__Java=http://worker:8080
|
||||||
|
- Workers__Kotlin=http://worker:8080
|
||||||
|
- Workers__CSharp=http://worker:8080
|
||||||
|
- Workers__Python=http://worker:8080
|
||||||
networks:
|
networks:
|
||||||
- liquidcode-network
|
- liquidcode-network
|
||||||
depends_on:
|
depends_on:
|
||||||
- worker-cpp
|
- worker
|
||||||
|
|
||||||
worker-cpp:
|
worker:
|
||||||
image: liquidcode-tester-worker-cpp:latest
|
image: liquidcode-tester-worker:latest
|
||||||
container_name: liquidcode-tester-worker-cpp
|
container_name: liquidcode-tester-worker
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: src/LiquidCode.Tester.Worker/Dockerfile
|
dockerfile: src/LiquidCode.Tester.Worker/Dockerfile
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"sdk": {
|
"sdk": {
|
||||||
"version": "9.0.0",
|
"version": "9.0.0",
|
||||||
"rollForward": "latestMinor"
|
"rollForward": "latestMajor"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
# Kubernetes Deployment
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- Kubernetes cluster (minikube, kind, or cloud provider)
|
|
||||||
- kubectl configured
|
|
||||||
- Docker images built and available
|
|
||||||
|
|
||||||
## Building Docker Images
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build Gateway image
|
|
||||||
docker build -t liquidcode-tester-gateway:latest -f src/LiquidCode.Tester.Gateway/Dockerfile .
|
|
||||||
|
|
||||||
# Build C++ Worker image
|
|
||||||
docker build -t liquidcode-tester-worker-cpp:latest -f src/LiquidCode.Tester.Worker/Dockerfile .
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deploying to Kubernetes
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create namespace
|
|
||||||
kubectl apply -f k8s/namespace.yaml
|
|
||||||
|
|
||||||
# Apply ConfigMap
|
|
||||||
kubectl apply -f k8s/configmap.yaml
|
|
||||||
|
|
||||||
# Deploy Worker (must be deployed first)
|
|
||||||
kubectl apply -f k8s/worker-cpp-deployment.yaml
|
|
||||||
|
|
||||||
# Deploy Gateway
|
|
||||||
kubectl apply -f k8s/gateway-deployment.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
## Checking Status
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check all resources
|
|
||||||
kubectl get all -n liquidcode-tester
|
|
||||||
|
|
||||||
# Check pods
|
|
||||||
kubectl get pods -n liquidcode-tester
|
|
||||||
|
|
||||||
# Check services
|
|
||||||
kubectl get services -n liquidcode-tester
|
|
||||||
|
|
||||||
# View logs
|
|
||||||
kubectl logs -n liquidcode-tester -l app=gateway
|
|
||||||
kubectl logs -n liquidcode-tester -l app=worker-cpp
|
|
||||||
```
|
|
||||||
|
|
||||||
## Access the Gateway
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Get the external IP (for LoadBalancer)
|
|
||||||
kubectl get service liquidcode-tester-gateway -n liquidcode-tester
|
|
||||||
|
|
||||||
# For minikube
|
|
||||||
minikube service liquidcode-tester-gateway -n liquidcode-tester
|
|
||||||
|
|
||||||
# Port forward (alternative)
|
|
||||||
kubectl port-forward -n liquidcode-tester service/liquidcode-tester-gateway 8080:80
|
|
||||||
```
|
|
||||||
|
|
||||||
## Scaling Workers
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Scale C++ workers
|
|
||||||
kubectl scale deployment liquidcode-tester-worker-cpp -n liquidcode-tester --replicas=5
|
|
||||||
```
|
|
||||||
|
|
||||||
## Cleanup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Delete all resources
|
|
||||||
kubectl delete namespace liquidcode-tester
|
|
||||||
```
|
|
||||||
|
|
||||||
## Production Considerations
|
|
||||||
|
|
||||||
1. **Image Registry**: Push images to a container registry (Docker Hub, GCR, ECR, etc.)
|
|
||||||
2. **Resource Limits**: Adjust CPU/Memory limits based on workload
|
|
||||||
3. **Persistent Storage**: Add PersistentVolumes for package storage if needed
|
|
||||||
4. **Monitoring**: Add Prometheus/Grafana for metrics
|
|
||||||
5. **Logging**: Configure centralized logging (ELK, Loki, etc.)
|
|
||||||
6. **Security**:
|
|
||||||
- Use NetworkPolicies to restrict traffic
|
|
||||||
- Enable Pod Security Standards
|
|
||||||
- Use secrets for sensitive data
|
|
||||||
- Consider using a service mesh (Istio, Linkerd)
|
|
||||||
7. **Autoscaling**: Configure HorizontalPodAutoscaler for workers
|
|
||||||
8. **Ingress**: Use Ingress controller instead of LoadBalancer for production
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: liquidcode-tester-config
|
|
||||||
namespace: liquidcode-tester
|
|
||||||
data:
|
|
||||||
gateway.appsettings.json: |
|
|
||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*",
|
|
||||||
"PackageDownloadDirectory": "/tmp/packages",
|
|
||||||
"Workers": {
|
|
||||||
"Cpp": "http://liquidcode-tester-worker-cpp:8080",
|
|
||||||
"Java": "http://liquidcode-tester-worker-java:8080",
|
|
||||||
"Kotlin": "http://liquidcode-tester-worker-kotlin:8080",
|
|
||||||
"CSharp": "http://liquidcode-tester-worker-csharp:8080"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
worker.appsettings.json: |
|
|
||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*",
|
|
||||||
"Cpp": {
|
|
||||||
"Compiler": "g++",
|
|
||||||
"CompilerFlags": "-O2 -std=c++17 -Wall"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: liquidcode-tester-gateway
|
|
||||||
namespace: liquidcode-tester
|
|
||||||
labels:
|
|
||||||
app: gateway
|
|
||||||
spec:
|
|
||||||
replicas: 2
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: gateway
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: gateway
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: gateway
|
|
||||||
image: liquidcode-tester-gateway:latest
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
ports:
|
|
||||||
- containerPort: 8080
|
|
||||||
name: http
|
|
||||||
env:
|
|
||||||
- name: ASPNETCORE_ENVIRONMENT
|
|
||||||
value: "Production"
|
|
||||||
- name: Workers__Cpp
|
|
||||||
value: "http://liquidcode-tester-worker-cpp:8080"
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
memory: "128Mi"
|
|
||||||
cpu: "100m"
|
|
||||||
limits:
|
|
||||||
memory: "512Mi"
|
|
||||||
cpu: "500m"
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /api/tester/health
|
|
||||||
port: 8080
|
|
||||||
initialDelaySeconds: 10
|
|
||||||
periodSeconds: 30
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /api/tester/health
|
|
||||||
port: 8080
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 10
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: liquidcode-tester-gateway
|
|
||||||
namespace: liquidcode-tester
|
|
||||||
spec:
|
|
||||||
type: LoadBalancer
|
|
||||||
selector:
|
|
||||||
app: gateway
|
|
||||||
ports:
|
|
||||||
- port: 80
|
|
||||||
targetPort: 8080
|
|
||||||
protocol: TCP
|
|
||||||
name: http
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: liquidcode-tester
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: liquidcode-tester-worker-cpp
|
|
||||||
namespace: liquidcode-tester
|
|
||||||
labels:
|
|
||||||
app: worker-cpp
|
|
||||||
language: cpp
|
|
||||||
spec:
|
|
||||||
replicas: 3
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: worker-cpp
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: worker-cpp
|
|
||||||
language: cpp
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: worker-cpp
|
|
||||||
image: liquidcode-tester-worker-cpp:latest
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
ports:
|
|
||||||
- containerPort: 8080
|
|
||||||
name: http
|
|
||||||
env:
|
|
||||||
- name: ASPNETCORE_ENVIRONMENT
|
|
||||||
value: "Production"
|
|
||||||
- name: Cpp__Compiler
|
|
||||||
value: "g++"
|
|
||||||
- name: Cpp__CompilerFlags
|
|
||||||
value: "-O2 -std=c++17 -Wall"
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
memory: "256Mi"
|
|
||||||
cpu: "200m"
|
|
||||||
limits:
|
|
||||||
memory: "2Gi"
|
|
||||||
cpu: "1000m"
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /api/test/health
|
|
||||||
port: 8080
|
|
||||||
initialDelaySeconds: 10
|
|
||||||
periodSeconds: 30
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /api/test/health
|
|
||||||
port: 8080
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 10
|
|
||||||
# Security context for isolation
|
|
||||||
securityContext:
|
|
||||||
allowPrivilegeEscalation: false
|
|
||||||
runAsNonRoot: true
|
|
||||||
runAsUser: 1000
|
|
||||||
capabilities:
|
|
||||||
drop:
|
|
||||||
- ALL
|
|
||||||
readOnlyRootFilesystem: false
|
|
||||||
volumeMounts:
|
|
||||||
- name: tmp
|
|
||||||
mountPath: /tmp
|
|
||||||
volumes:
|
|
||||||
- name: tmp
|
|
||||||
emptyDir: {}
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: liquidcode-tester-worker-cpp
|
|
||||||
namespace: liquidcode-tester
|
|
||||||
spec:
|
|
||||||
type: ClusterIP
|
|
||||||
selector:
|
|
||||||
app: worker-cpp
|
|
||||||
ports:
|
|
||||||
- port: 8080
|
|
||||||
targetPort: 8080
|
|
||||||
protocol: TCP
|
|
||||||
name: http
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using LiquidCode.Tester.Common.Models;
|
using LiquidCode.Tester.Common.Models;
|
||||||
|
using LiquidCode.Tester.Gateway.Models;
|
||||||
using LiquidCode.Tester.Gateway.Services;
|
using LiquidCode.Tester.Gateway.Services;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@@ -44,6 +45,58 @@ public class TesterController : ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("submit-local")]
|
||||||
|
public async Task<IActionResult> SubmitLocal([FromForm] LocalSubmitModel request)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Received local submit request for ID {SubmitId}", request.Id);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (request.Package == null)
|
||||||
|
{
|
||||||
|
return BadRequest(new { error = "Package file is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save uploaded package to temporary file
|
||||||
|
var packageDirectory = Path.Combine(Path.GetTempPath(), "packages");
|
||||||
|
if (!Directory.Exists(packageDirectory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(packageDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
var packageFileName = $"package_{Guid.NewGuid()}.zip";
|
||||||
|
var packagePath = Path.Combine(packageDirectory, packageFileName);
|
||||||
|
|
||||||
|
await using (var fileStream = new FileStream(packagePath, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
|
{
|
||||||
|
await request.Package.CopyToAsync(fileStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Package saved to {PackagePath}", packagePath);
|
||||||
|
|
||||||
|
// Create SubmitForTesterModel from local submit
|
||||||
|
var submitModel = new SubmitForTesterModel(
|
||||||
|
Id: request.Id,
|
||||||
|
MissionId: request.MissionId,
|
||||||
|
Language: request.Language,
|
||||||
|
LanguageVersion: request.LanguageVersion,
|
||||||
|
SourceCode: request.SourceCode,
|
||||||
|
PackageUrl: packagePath, // Use local path instead of URL
|
||||||
|
CallbackUrl: request.CallbackUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send to appropriate worker based on language
|
||||||
|
await _workerClientService.SendToWorkerAsync(submitModel, packagePath);
|
||||||
|
|
||||||
|
return Accepted(new { message = "Submit accepted for testing", submitId = request.Id });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to process local submit {SubmitId}", request.Id);
|
||||||
|
return StatusCode(500, new { error = "Failed to process submit", details = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("health")]
|
[HttpGet("health")]
|
||||||
public IActionResult Health()
|
public IActionResult Health()
|
||||||
{
|
{
|
||||||
|
|||||||
12
src/LiquidCode.Tester.Gateway/Models/LocalSubmitModel.cs
Normal file
12
src/LiquidCode.Tester.Gateway/Models/LocalSubmitModel.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace LiquidCode.Tester.Gateway.Models;
|
||||||
|
|
||||||
|
public class LocalSubmitModel
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public long MissionId { get; set; }
|
||||||
|
public string Language { get; set; } = string.Empty;
|
||||||
|
public string LanguageVersion { get; set; } = string.Empty;
|
||||||
|
public string SourceCode { get; set; } = string.Empty;
|
||||||
|
public string CallbackUrl { get; set; } = string.Empty;
|
||||||
|
public IFormFile? Package { get; set; }
|
||||||
|
}
|
||||||
@@ -79,6 +79,7 @@ public class WorkerClientService : IWorkerClientService
|
|||||||
"java" => _configuration["Workers:Java"],
|
"java" => _configuration["Workers:Java"],
|
||||||
"kotlin" => _configuration["Workers:Kotlin"],
|
"kotlin" => _configuration["Workers:Kotlin"],
|
||||||
"c#" => _configuration["Workers:CSharp"],
|
"c#" => _configuration["Workers:CSharp"],
|
||||||
|
"python" => _configuration["Workers:Python"],
|
||||||
_ => throw new NotSupportedException($"Language {language} is not supported")
|
_ => throw new NotSupportedException($"Language {language} is not supported")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,10 @@
|
|||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"PackageDownloadDirectory": "/tmp/packages",
|
"PackageDownloadDirectory": "/tmp/packages",
|
||||||
"Workers": {
|
"Workers": {
|
||||||
"Cpp": "http://liquidcode-tester-worker-cpp:8080",
|
"Cpp": "http://localhost:8081",
|
||||||
"Java": "http://liquidcode-tester-worker-java:8080",
|
"Java": "http://localhost:8081",
|
||||||
"Kotlin": "http://liquidcode-tester-worker-kotlin:8080",
|
"Kotlin": "http://localhost:8081",
|
||||||
"CSharp": "http://liquidcode-tester-worker-csharp:8080"
|
"CSharp": "http://localhost:8081",
|
||||||
|
"Python": "http://localhost:8081"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,16 +23,63 @@ public class TestController : ControllerBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Save package to temporary file before starting background task
|
||||||
|
// This is necessary because IFormFile becomes unavailable after request completes
|
||||||
|
string? packageFilePath = null;
|
||||||
|
if (request.Package != null)
|
||||||
|
{
|
||||||
|
var tempDirectory = Path.Combine(Path.GetTempPath(), "worker-packages");
|
||||||
|
Directory.CreateDirectory(tempDirectory);
|
||||||
|
|
||||||
|
packageFilePath = Path.Combine(tempDirectory, $"package_{Guid.NewGuid()}.zip");
|
||||||
|
|
||||||
|
await using (var fileStream = new FileStream(packageFilePath, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
|
{
|
||||||
|
await request.Package.CopyToAsync(fileStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Package saved to temporary file: {FilePath}", packageFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a copy of request data for background task
|
||||||
|
var backgroundRequest = new TestRequest
|
||||||
|
{
|
||||||
|
Id = request.Id,
|
||||||
|
MissionId = request.MissionId,
|
||||||
|
Language = request.Language,
|
||||||
|
LanguageVersion = request.LanguageVersion,
|
||||||
|
SourceCode = request.SourceCode,
|
||||||
|
CallbackUrl = request.CallbackUrl,
|
||||||
|
Package = null, // Will use file path instead
|
||||||
|
PackageFilePath = packageFilePath
|
||||||
|
};
|
||||||
|
|
||||||
// Start testing in background
|
// Start testing in background
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _testingService.ProcessSubmitAsync(request);
|
await _testingService.ProcessSubmitAsync(backgroundRequest);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error processing submit {SubmitId}", request.Id);
|
_logger.LogError(ex, "Error processing submit {SubmitId}", backgroundRequest.Id);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Cleanup temporary package file
|
||||||
|
if (packageFilePath != null && System.IO.File.Exists(packageFilePath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
System.IO.File.Delete(packageFilePath);
|
||||||
|
_logger.LogInformation("Deleted temporary package file: {FilePath}", packageFilePath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to delete temporary package file: {FilePath}", packageFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,7 +95,7 @@ public class TestController : ControllerBase
|
|||||||
[HttpGet("health")]
|
[HttpGet("health")]
|
||||||
public IActionResult Health()
|
public IActionResult Health()
|
||||||
{
|
{
|
||||||
return Ok(new { status = "healthy", service = "cpp-worker", timestamp = DateTime.UtcNow });
|
return Ok(new { status = "healthy", service = "universal-worker", timestamp = DateTime.UtcNow });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,4 +108,5 @@ public class TestRequest
|
|||||||
public string SourceCode { get; set; } = string.Empty;
|
public string SourceCode { get; set; } = string.Empty;
|
||||||
public string CallbackUrl { get; set; } = string.Empty;
|
public string CallbackUrl { get; set; } = string.Empty;
|
||||||
public IFormFile? Package { get; set; }
|
public IFormFile? Package { get; set; }
|
||||||
|
public string? PackageFilePath { get; set; } // Internal use - path to saved package file
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,16 +24,44 @@ FROM build AS publish
|
|||||||
ARG BUILD_CONFIGURATION=Release
|
ARG BUILD_CONFIGURATION=Release
|
||||||
RUN dotnet publish "./LiquidCode.Tester.Worker.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
RUN dotnet publish "./LiquidCode.Tester.Worker.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||||
|
|
||||||
# Final stage - use aspnet runtime with C++ compiler
|
# Final stage - use aspnet runtime with all compilers
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final
|
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install C++ compiler and build tools
|
# Install compilers and runtimes for all supported languages
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
|
# C++ compiler and build tools
|
||||||
g++ \
|
g++ \
|
||||||
gcc \
|
gcc \
|
||||||
make \
|
make \
|
||||||
|
# Java Development Kit and Runtime
|
||||||
|
openjdk-17-jdk \
|
||||||
|
# Python
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
# Kotlin compiler
|
||||||
|
wget \
|
||||||
|
unzip \
|
||||||
|
&& wget -q https://github.com/JetBrains/kotlin/releases/download/v1.9.20/kotlin-compiler-1.9.20.zip -O /tmp/kotlin.zip \
|
||||||
|
&& unzip -q /tmp/kotlin.zip -d /opt \
|
||||||
|
&& rm /tmp/kotlin.zip \
|
||||||
|
&& ln -s /opt/kotlinc/bin/kotlinc /usr/local/bin/kotlinc \
|
||||||
|
&& ln -s /opt/kotlinc/bin/kotlin /usr/local/bin/kotlin \
|
||||||
|
# Cleanup
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install Mono for C# compilation (csc)
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
gnupg \
|
||||||
|
&& gpg --homedir /tmp --no-default-keyring --keyring /usr/share/keyrings/mono-official-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF \
|
||||||
|
&& echo "deb [signed-by=/usr/share/keyrings/mono-official-archive-keyring.gpg] https://download.mono-project.com/repo/debian stable-buster main" | tee /etc/apt/sources.list.d/mono-official-stable.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends mono-devel \
|
||||||
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy published app
|
# Copy published app
|
||||||
|
|||||||
15
src/LiquidCode.Tester.Worker/Models/LanguageConfig.cs
Normal file
15
src/LiquidCode.Tester.Worker/Models/LanguageConfig.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace LiquidCode.Tester.Worker.Models;
|
||||||
|
|
||||||
|
public class LanguageConfig
|
||||||
|
{
|
||||||
|
public string DefaultVersion { get; set; } = "latest";
|
||||||
|
public Dictionary<string, VersionConfig> Versions { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VersionConfig
|
||||||
|
{
|
||||||
|
public string? Compiler { get; set; }
|
||||||
|
public string? CompilerFlags { get; set; }
|
||||||
|
public string? Executable { get; set; }
|
||||||
|
public string? Runtime { get; set; }
|
||||||
|
}
|
||||||
@@ -11,10 +11,26 @@ builder.Services.AddHttpClient();
|
|||||||
|
|
||||||
// Register application services
|
// Register application services
|
||||||
builder.Services.AddSingleton<IPackageParserService, PackageParserService>();
|
builder.Services.AddSingleton<IPackageParserService, PackageParserService>();
|
||||||
builder.Services.AddSingleton<ICompilationService, CppCompilationService>();
|
|
||||||
builder.Services.AddSingleton<IExecutionService, CppExecutionService>();
|
|
||||||
builder.Services.AddSingleton<IOutputCheckerService, OutputCheckerService>();
|
builder.Services.AddSingleton<IOutputCheckerService, OutputCheckerService>();
|
||||||
builder.Services.AddSingleton<ICallbackService, CallbackService>();
|
builder.Services.AddSingleton<ICallbackService, CallbackService>();
|
||||||
|
|
||||||
|
// Register compilation services
|
||||||
|
builder.Services.AddSingleton<CppCompilationService>();
|
||||||
|
builder.Services.AddSingleton<JavaCompilationService>();
|
||||||
|
builder.Services.AddSingleton<KotlinCompilationService>();
|
||||||
|
builder.Services.AddSingleton<CSharpCompilationService>();
|
||||||
|
builder.Services.AddSingleton<PythonCompilationService>();
|
||||||
|
builder.Services.AddSingleton<ICompilationServiceFactory, CompilationServiceFactory>();
|
||||||
|
|
||||||
|
// Register execution services
|
||||||
|
builder.Services.AddSingleton<CppExecutionService>();
|
||||||
|
builder.Services.AddSingleton<JavaExecutionService>();
|
||||||
|
builder.Services.AddSingleton<KotlinExecutionService>();
|
||||||
|
builder.Services.AddSingleton<CSharpExecutionService>();
|
||||||
|
builder.Services.AddSingleton<PythonExecutionService>();
|
||||||
|
builder.Services.AddSingleton<IExecutionServiceFactory, ExecutionServiceFactory>();
|
||||||
|
|
||||||
|
// Register testing service
|
||||||
builder.Services.AddSingleton<ITestingService, TestingService>();
|
builder.Services.AddSingleton<ITestingService, TestingService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using LiquidCode.Tester.Worker.Models;
|
||||||
|
|
||||||
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
|
|
||||||
|
public class CSharpCompilationService : ICompilationService
|
||||||
|
{
|
||||||
|
private readonly ILogger<CSharpCompilationService> _logger;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
public CSharpCompilationService(ILogger<CSharpCompilationService> logger, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CompilationResult> CompileAsync(string sourceCode, string workingDirectory, string? version = null)
|
||||||
|
{
|
||||||
|
var sourceFilePath = Path.Combine(workingDirectory, "Solution.cs");
|
||||||
|
var executablePath = Path.Combine(workingDirectory, "solution.exe");
|
||||||
|
|
||||||
|
_logger.LogInformation("Compiling C# code in {WorkingDirectory} with version {Version}", workingDirectory, version ?? "latest");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await File.WriteAllTextAsync(sourceFilePath, sourceCode);
|
||||||
|
|
||||||
|
// Resolve version-specific configuration
|
||||||
|
var (compiler, compilerFlags) = ResolveVersion(version);
|
||||||
|
|
||||||
|
_logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, compilerFlags);
|
||||||
|
|
||||||
|
var arguments = $"{compilerFlags} /out:\"{executablePath}\" \"{sourceFilePath}\"";
|
||||||
|
|
||||||
|
var process = new Process
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = compiler,
|
||||||
|
Arguments = arguments,
|
||||||
|
WorkingDirectory = workingDirectory,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
|
||||||
|
var output = await process.StandardOutput.ReadToEndAsync();
|
||||||
|
var error = await process.StandardError.ReadToEndAsync();
|
||||||
|
|
||||||
|
await process.WaitForExitAsync();
|
||||||
|
|
||||||
|
var compilerOutput = $"{output}\n{error}".Trim();
|
||||||
|
|
||||||
|
if (process.ExitCode == 0 && File.Exists(executablePath))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("C# compilation successful");
|
||||||
|
return new CompilationResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
ExecutablePath = executablePath,
|
||||||
|
CompilerOutput = compilerOutput
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("C# compilation failed with exit code {ExitCode}", process.ExitCode);
|
||||||
|
return new CompilationResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
ErrorMessage = "Compilation failed",
|
||||||
|
CompilerOutput = compilerOutput
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error during C# compilation");
|
||||||
|
return new CompilationResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
ErrorMessage = $"Compilation error: {ex.Message}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private (string compiler, string compilerFlags) ResolveVersion(string? version)
|
||||||
|
{
|
||||||
|
// If version is null or "latest", use default configuration
|
||||||
|
if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var compiler = _configuration["CSharp:Compiler"] ?? "csc";
|
||||||
|
var compilerFlags = _configuration["CSharp:CompilerFlags"] ?? "/optimize+";
|
||||||
|
return (compiler, compilerFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find version-specific configuration
|
||||||
|
var versionKey = $"CSharp:Versions:{version}";
|
||||||
|
var versionCompiler = _configuration[$"{versionKey}:Compiler"];
|
||||||
|
var versionFlags = _configuration[$"{versionKey}:CompilerFlags"];
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(versionCompiler))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Using C# version {Version} configuration", version);
|
||||||
|
return (versionCompiler, versionFlags ?? "/optimize+");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version not found, use default and log warning
|
||||||
|
_logger.LogWarning("C# version {Version} not found in configuration, using default", version);
|
||||||
|
var defaultCompiler = _configuration["CSharp:Compiler"] ?? "csc";
|
||||||
|
var defaultFlags = _configuration["CSharp:CompilerFlags"] ?? "/optimize+";
|
||||||
|
return (defaultCompiler, defaultFlags);
|
||||||
|
}
|
||||||
|
}
|
||||||
108
src/LiquidCode.Tester.Worker/Services/CSharpExecutionService.cs
Normal file
108
src/LiquidCode.Tester.Worker/Services/CSharpExecutionService.cs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
|
|
||||||
|
public class CSharpExecutionService : IExecutionService
|
||||||
|
{
|
||||||
|
private readonly ILogger<CSharpExecutionService> _logger;
|
||||||
|
|
||||||
|
public CSharpExecutionService(ILogger<CSharpExecutionService> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ExecutionResult> ExecuteAsync(string executablePath, string inputFilePath, int timeLimitMs, int memoryLimitMb)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Executing C# executable {Executable} with input {Input}, time limit {TimeLimit}ms, memory limit {MemoryLimit}MB",
|
||||||
|
executablePath, inputFilePath, timeLimitMs, memoryLimitMb);
|
||||||
|
|
||||||
|
var result = new ExecutionResult();
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var inputStream = File.OpenRead(inputFilePath);
|
||||||
|
|
||||||
|
var process = new Process
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = executablePath,
|
||||||
|
RedirectStandardInput = true,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
|
||||||
|
await inputStream.CopyToAsync(process.StandardInput.BaseStream);
|
||||||
|
process.StandardInput.Close();
|
||||||
|
|
||||||
|
var outputTask = process.StandardOutput.ReadToEndAsync();
|
||||||
|
var errorTask = process.StandardError.ReadToEndAsync();
|
||||||
|
|
||||||
|
var completedInTime = await Task.Run(() => process.WaitForExit(timeLimitMs));
|
||||||
|
|
||||||
|
stopwatch.Stop();
|
||||||
|
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
if (!completedInTime)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
process.Kill(entireProcessTree: true);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
result.TimeLimitExceeded = true;
|
||||||
|
result.ErrorMessage = "Time limit exceeded";
|
||||||
|
_logger.LogWarning("Execution exceeded time limit");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Output = await outputTask;
|
||||||
|
result.ErrorOutput = await errorTask;
|
||||||
|
result.ExitCode = process.ExitCode;
|
||||||
|
|
||||||
|
if (process.ExitCode != 0)
|
||||||
|
{
|
||||||
|
result.RuntimeError = true;
|
||||||
|
result.ErrorMessage = $"Runtime error (exit code {process.ExitCode})";
|
||||||
|
_logger.LogWarning("Runtime error with exit code {ExitCode}", process.ExitCode);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result.MemoryUsedMb = process.PeakWorkingSet64 / (1024 * 1024);
|
||||||
|
if (result.MemoryUsedMb > memoryLimitMb)
|
||||||
|
{
|
||||||
|
result.MemoryLimitExceeded = true;
|
||||||
|
result.ErrorMessage = "Memory limit exceeded";
|
||||||
|
_logger.LogWarning("Memory limit exceeded: {Used}MB > {Limit}MB", result.MemoryUsedMb, memoryLimitMb);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Could not measure memory usage");
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Success = true;
|
||||||
|
_logger.LogInformation("Execution completed successfully in {Time}ms", result.ExecutionTimeMs);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
|
||||||
|
result.RuntimeError = true;
|
||||||
|
result.ErrorMessage = $"Execution error: {ex.Message}";
|
||||||
|
_logger.LogError(ex, "Error during execution");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,13 @@ public class CallbackService : ICallbackService
|
|||||||
|
|
||||||
public async Task SendStatusAsync(string callbackUrl, TesterResponseModel response)
|
public async Task SendStatusAsync(string callbackUrl, TesterResponseModel response)
|
||||||
{
|
{
|
||||||
|
// Check if callback should be logged instead of sent via HTTP
|
||||||
|
if (IsLogCallback(callbackUrl))
|
||||||
|
{
|
||||||
|
LogCallback(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Sending status update to {CallbackUrl} for submit {SubmitId}", callbackUrl, response.SubmitId);
|
_logger.LogInformation("Sending status update to {CallbackUrl} for submit {SubmitId}", callbackUrl, response.SubmitId);
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -36,4 +43,37 @@ public class CallbackService : ICallbackService
|
|||||||
// Don't throw - callback failures shouldn't stop testing
|
// Don't throw - callback failures shouldn't stop testing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsLogCallback(string callbackUrl)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(callbackUrl))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = callbackUrl.Trim().ToLowerInvariant();
|
||||||
|
return normalized == "log" ||
|
||||||
|
normalized == "console" ||
|
||||||
|
normalized.StartsWith("log://");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogCallback(TesterResponseModel response)
|
||||||
|
{
|
||||||
|
var options = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(response, options);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"\n" +
|
||||||
|
"╔═══════════════════════════════════════════════════════════════╗\n" +
|
||||||
|
"║ CALLBACK RESULT ║\n" +
|
||||||
|
"╠═══════════════════════════════════════════════════════════════╣\n" +
|
||||||
|
"{Json}\n" +
|
||||||
|
"╚═══════════════════════════════════════════════════════════════╝",
|
||||||
|
json);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
|
|
||||||
|
public class CompilationServiceFactory : ICompilationServiceFactory
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly ILogger<CompilationServiceFactory> _logger;
|
||||||
|
|
||||||
|
public CompilationServiceFactory(IServiceProvider serviceProvider, ILogger<CompilationServiceFactory> logger)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICompilationService GetCompilationService(string language)
|
||||||
|
{
|
||||||
|
var normalizedLanguage = language.ToLowerInvariant().Replace(" ", "");
|
||||||
|
|
||||||
|
_logger.LogInformation("Getting compilation service for language: {Language}", normalizedLanguage);
|
||||||
|
|
||||||
|
return normalizedLanguage switch
|
||||||
|
{
|
||||||
|
"c++" or "cpp" => _serviceProvider.GetRequiredService<CppCompilationService>(),
|
||||||
|
"java" => _serviceProvider.GetRequiredService<JavaCompilationService>(),
|
||||||
|
"kotlin" => _serviceProvider.GetRequiredService<KotlinCompilationService>(),
|
||||||
|
"c#" or "csharp" => _serviceProvider.GetRequiredService<CSharpCompilationService>(),
|
||||||
|
"python" => _serviceProvider.GetRequiredService<PythonCompilationService>(),
|
||||||
|
_ => throw new NotSupportedException($"Language '{language}' is not supported")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using LiquidCode.Tester.Worker.Models;
|
||||||
|
|
||||||
namespace LiquidCode.Tester.Worker.Services;
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
|
|
||||||
@@ -13,21 +14,22 @@ public class CppCompilationService : ICompilationService
|
|||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CompilationResult> CompileAsync(string sourceCode, string workingDirectory)
|
public async Task<CompilationResult> CompileAsync(string sourceCode, string workingDirectory, string? version = null)
|
||||||
{
|
{
|
||||||
var sourceFilePath = Path.Combine(workingDirectory, "solution.cpp");
|
var sourceFilePath = Path.Combine(workingDirectory, "solution.cpp");
|
||||||
var executablePath = Path.Combine(workingDirectory, "solution");
|
var executablePath = Path.Combine(workingDirectory, "solution");
|
||||||
|
|
||||||
_logger.LogInformation("Compiling C++ code in {WorkingDirectory}", workingDirectory);
|
_logger.LogInformation("Compiling C++ code in {WorkingDirectory} with version {Version}", workingDirectory, version ?? "latest");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Write source code to file
|
// Write source code to file
|
||||||
await File.WriteAllTextAsync(sourceFilePath, sourceCode);
|
await File.WriteAllTextAsync(sourceFilePath, sourceCode);
|
||||||
|
|
||||||
// Compile using g++
|
// Resolve version-specific configuration
|
||||||
var compiler = _configuration["Cpp:Compiler"] ?? "g++";
|
var (compiler, compilerFlags) = ResolveVersion(version);
|
||||||
var compilerFlags = _configuration["Cpp:CompilerFlags"] ?? "-O2 -std=c++17 -Wall";
|
|
||||||
|
_logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, compilerFlags);
|
||||||
|
|
||||||
var process = new Process
|
var process = new Process
|
||||||
{
|
{
|
||||||
@@ -83,4 +85,32 @@ public class CppCompilationService : ICompilationService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private (string compiler, string compilerFlags) ResolveVersion(string? version)
|
||||||
|
{
|
||||||
|
// If version is null or "latest", use default configuration
|
||||||
|
if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var compiler = _configuration["Cpp:Compiler"] ?? "g++";
|
||||||
|
var compilerFlags = _configuration["Cpp:CompilerFlags"] ?? "-O2 -std=c++17 -Wall";
|
||||||
|
return (compiler, compilerFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find version-specific configuration
|
||||||
|
var versionKey = $"Cpp:Versions:{version}";
|
||||||
|
var versionCompiler = _configuration[$"{versionKey}:Compiler"];
|
||||||
|
var versionFlags = _configuration[$"{versionKey}:CompilerFlags"];
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(versionCompiler))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Using C++ version {Version} configuration", version);
|
||||||
|
return (versionCompiler, versionFlags ?? "-O2 -Wall");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version not found, use default and log warning
|
||||||
|
_logger.LogWarning("C++ version {Version} not found in configuration, using default", version);
|
||||||
|
var defaultCompiler = _configuration["Cpp:Compiler"] ?? "g++";
|
||||||
|
var defaultFlags = _configuration["Cpp:CompilerFlags"] ?? "-O2 -std=c++17 -Wall";
|
||||||
|
return (defaultCompiler, defaultFlags);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
|
|
||||||
|
public class ExecutionServiceFactory : IExecutionServiceFactory
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly ILogger<ExecutionServiceFactory> _logger;
|
||||||
|
|
||||||
|
public ExecutionServiceFactory(IServiceProvider serviceProvider, ILogger<ExecutionServiceFactory> logger)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IExecutionService GetExecutionService(string language)
|
||||||
|
{
|
||||||
|
var normalizedLanguage = language.ToLowerInvariant().Replace(" ", "");
|
||||||
|
|
||||||
|
_logger.LogInformation("Getting execution service for language: {Language}", normalizedLanguage);
|
||||||
|
|
||||||
|
return normalizedLanguage switch
|
||||||
|
{
|
||||||
|
"c++" or "cpp" => _serviceProvider.GetRequiredService<CppExecutionService>(),
|
||||||
|
"java" => _serviceProvider.GetRequiredService<JavaExecutionService>(),
|
||||||
|
"kotlin" => _serviceProvider.GetRequiredService<KotlinExecutionService>(),
|
||||||
|
"c#" or "csharp" => _serviceProvider.GetRequiredService<CSharpExecutionService>(),
|
||||||
|
"python" => _serviceProvider.GetRequiredService<PythonExecutionService>(),
|
||||||
|
_ => throw new NotSupportedException($"Language '{language}' is not supported")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,9 @@ public interface ICompilationService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sourceCode">Source code to compile</param>
|
/// <param name="sourceCode">Source code to compile</param>
|
||||||
/// <param name="workingDirectory">Directory to compile in</param>
|
/// <param name="workingDirectory">Directory to compile in</param>
|
||||||
|
/// <param name="version">Language version (e.g., "17", "20", "latest"). If null or "latest", uses default version.</param>
|
||||||
/// <returns>Result containing success status, executable path, and error messages</returns>
|
/// <returns>Result containing success status, executable path, and error messages</returns>
|
||||||
Task<CompilationResult> CompileAsync(string sourceCode, string workingDirectory);
|
Task<CompilationResult> CompileAsync(string sourceCode, string workingDirectory, string? version = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CompilationResult
|
public class CompilationResult
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
|
|
||||||
|
public interface ICompilationServiceFactory
|
||||||
|
{
|
||||||
|
ICompilationService GetCompilationService(string language);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
|
|
||||||
|
public interface IExecutionServiceFactory
|
||||||
|
{
|
||||||
|
IExecutionService GetExecutionService(string language);
|
||||||
|
}
|
||||||
119
src/LiquidCode.Tester.Worker/Services/JavaCompilationService.cs
Normal file
119
src/LiquidCode.Tester.Worker/Services/JavaCompilationService.cs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using LiquidCode.Tester.Worker.Models;
|
||||||
|
|
||||||
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
|
|
||||||
|
public class JavaCompilationService : ICompilationService
|
||||||
|
{
|
||||||
|
private readonly ILogger<JavaCompilationService> _logger;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
public JavaCompilationService(ILogger<JavaCompilationService> logger, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CompilationResult> CompileAsync(string sourceCode, string workingDirectory, string? version = null)
|
||||||
|
{
|
||||||
|
var sourceFilePath = Path.Combine(workingDirectory, "Solution.java");
|
||||||
|
var classFilePath = Path.Combine(workingDirectory, "Solution.class");
|
||||||
|
|
||||||
|
_logger.LogInformation("Compiling Java code in {WorkingDirectory} with version {Version}", workingDirectory, version ?? "latest");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await File.WriteAllTextAsync(sourceFilePath, sourceCode);
|
||||||
|
|
||||||
|
// Resolve version-specific configuration
|
||||||
|
var (compiler, compilerFlags) = ResolveVersion(version);
|
||||||
|
|
||||||
|
_logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, compilerFlags);
|
||||||
|
|
||||||
|
var arguments = string.IsNullOrWhiteSpace(compilerFlags)
|
||||||
|
? $"\"{sourceFilePath}\""
|
||||||
|
: $"{compilerFlags} \"{sourceFilePath}\"";
|
||||||
|
|
||||||
|
var process = new Process
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = compiler,
|
||||||
|
Arguments = arguments,
|
||||||
|
WorkingDirectory = workingDirectory,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
|
||||||
|
var output = await process.StandardOutput.ReadToEndAsync();
|
||||||
|
var error = await process.StandardError.ReadToEndAsync();
|
||||||
|
|
||||||
|
await process.WaitForExitAsync();
|
||||||
|
|
||||||
|
var compilerOutput = $"{output}\n{error}".Trim();
|
||||||
|
|
||||||
|
if (process.ExitCode == 0 && File.Exists(classFilePath))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Java compilation successful");
|
||||||
|
return new CompilationResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
ExecutablePath = classFilePath,
|
||||||
|
CompilerOutput = compilerOutput
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Java compilation failed with exit code {ExitCode}", process.ExitCode);
|
||||||
|
return new CompilationResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
ErrorMessage = "Compilation failed",
|
||||||
|
CompilerOutput = compilerOutput
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error during Java compilation");
|
||||||
|
return new CompilationResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
ErrorMessage = $"Compilation error: {ex.Message}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private (string compiler, string compilerFlags) ResolveVersion(string? version)
|
||||||
|
{
|
||||||
|
// If version is null or "latest", use default configuration
|
||||||
|
if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var compiler = _configuration["Java:Compiler"] ?? "javac";
|
||||||
|
var compilerFlags = _configuration["Java:CompilerFlags"] ?? "";
|
||||||
|
return (compiler, compilerFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find version-specific configuration
|
||||||
|
var versionKey = $"Java:Versions:{version}";
|
||||||
|
var versionCompiler = _configuration[$"{versionKey}:Compiler"];
|
||||||
|
var versionFlags = _configuration[$"{versionKey}:CompilerFlags"];
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(versionCompiler))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Using Java version {Version} configuration", version);
|
||||||
|
return (versionCompiler, versionFlags ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version not found, use default and log warning
|
||||||
|
_logger.LogWarning("Java version {Version} not found in configuration, using default", version);
|
||||||
|
var defaultCompiler = _configuration["Java:Compiler"] ?? "javac";
|
||||||
|
var defaultFlags = _configuration["Java:CompilerFlags"] ?? "";
|
||||||
|
return (defaultCompiler, defaultFlags);
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/LiquidCode.Tester.Worker/Services/JavaExecutionService.cs
Normal file
111
src/LiquidCode.Tester.Worker/Services/JavaExecutionService.cs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
|
|
||||||
|
public class JavaExecutionService : IExecutionService
|
||||||
|
{
|
||||||
|
private readonly ILogger<JavaExecutionService> _logger;
|
||||||
|
|
||||||
|
public JavaExecutionService(ILogger<JavaExecutionService> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ExecutionResult> ExecuteAsync(string executablePath, string inputFilePath, int timeLimitMs, int memoryLimitMb)
|
||||||
|
{
|
||||||
|
var workingDirectory = Path.GetDirectoryName(executablePath)!;
|
||||||
|
_logger.LogInformation("Executing Java class in {WorkingDirectory} with input {Input}, time limit {TimeLimit}ms, memory limit {MemoryLimit}MB",
|
||||||
|
workingDirectory, inputFilePath, timeLimitMs, memoryLimitMb);
|
||||||
|
|
||||||
|
var result = new ExecutionResult();
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var inputStream = File.OpenRead(inputFilePath);
|
||||||
|
|
||||||
|
var process = new Process
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "java",
|
||||||
|
Arguments = "-cp . Solution",
|
||||||
|
WorkingDirectory = workingDirectory,
|
||||||
|
RedirectStandardInput = true,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
|
||||||
|
await inputStream.CopyToAsync(process.StandardInput.BaseStream);
|
||||||
|
process.StandardInput.Close();
|
||||||
|
|
||||||
|
var outputTask = process.StandardOutput.ReadToEndAsync();
|
||||||
|
var errorTask = process.StandardError.ReadToEndAsync();
|
||||||
|
|
||||||
|
var completedInTime = await Task.Run(() => process.WaitForExit(timeLimitMs));
|
||||||
|
|
||||||
|
stopwatch.Stop();
|
||||||
|
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
if (!completedInTime)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
process.Kill(entireProcessTree: true);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
result.TimeLimitExceeded = true;
|
||||||
|
result.ErrorMessage = "Time limit exceeded";
|
||||||
|
_logger.LogWarning("Execution exceeded time limit");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Output = await outputTask;
|
||||||
|
result.ErrorOutput = await errorTask;
|
||||||
|
result.ExitCode = process.ExitCode;
|
||||||
|
|
||||||
|
if (process.ExitCode != 0)
|
||||||
|
{
|
||||||
|
result.RuntimeError = true;
|
||||||
|
result.ErrorMessage = $"Runtime error (exit code {process.ExitCode})";
|
||||||
|
_logger.LogWarning("Runtime error with exit code {ExitCode}", process.ExitCode);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result.MemoryUsedMb = process.PeakWorkingSet64 / (1024 * 1024);
|
||||||
|
if (result.MemoryUsedMb > memoryLimitMb)
|
||||||
|
{
|
||||||
|
result.MemoryLimitExceeded = true;
|
||||||
|
result.ErrorMessage = "Memory limit exceeded";
|
||||||
|
_logger.LogWarning("Memory limit exceeded: {Used}MB > {Limit}MB", result.MemoryUsedMb, memoryLimitMb);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Could not measure memory usage");
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Success = true;
|
||||||
|
_logger.LogInformation("Execution completed successfully in {Time}ms", result.ExecutionTimeMs);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
|
||||||
|
result.RuntimeError = true;
|
||||||
|
result.ErrorMessage = $"Execution error: {ex.Message}";
|
||||||
|
_logger.LogError(ex, "Error during execution");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using LiquidCode.Tester.Worker.Models;
|
||||||
|
|
||||||
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
|
|
||||||
|
public class KotlinCompilationService : ICompilationService
|
||||||
|
{
|
||||||
|
private readonly ILogger<KotlinCompilationService> _logger;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
public KotlinCompilationService(ILogger<KotlinCompilationService> logger, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CompilationResult> CompileAsync(string sourceCode, string workingDirectory, string? version = null)
|
||||||
|
{
|
||||||
|
var sourceFilePath = Path.Combine(workingDirectory, "Solution.kt");
|
||||||
|
var jarFilePath = Path.Combine(workingDirectory, "solution.jar");
|
||||||
|
|
||||||
|
_logger.LogInformation("Compiling Kotlin code in {WorkingDirectory} with version {Version}", workingDirectory, version ?? "latest");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await File.WriteAllTextAsync(sourceFilePath, sourceCode);
|
||||||
|
|
||||||
|
// Resolve version-specific configuration
|
||||||
|
var (compiler, compilerFlags) = ResolveVersion(version);
|
||||||
|
|
||||||
|
_logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, compilerFlags);
|
||||||
|
|
||||||
|
var arguments = string.IsNullOrWhiteSpace(compilerFlags)
|
||||||
|
? $"\"{sourceFilePath}\" -include-runtime -d \"{jarFilePath}\""
|
||||||
|
: $"{compilerFlags} \"{sourceFilePath}\" -include-runtime -d \"{jarFilePath}\"";
|
||||||
|
|
||||||
|
var process = new Process
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = compiler,
|
||||||
|
Arguments = arguments,
|
||||||
|
WorkingDirectory = workingDirectory,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
|
||||||
|
var output = await process.StandardOutput.ReadToEndAsync();
|
||||||
|
var error = await process.StandardError.ReadToEndAsync();
|
||||||
|
|
||||||
|
await process.WaitForExitAsync();
|
||||||
|
|
||||||
|
var compilerOutput = $"{output}\n{error}".Trim();
|
||||||
|
|
||||||
|
if (process.ExitCode == 0 && File.Exists(jarFilePath))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Kotlin compilation successful");
|
||||||
|
return new CompilationResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
ExecutablePath = jarFilePath,
|
||||||
|
CompilerOutput = compilerOutput
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Kotlin compilation failed with exit code {ExitCode}", process.ExitCode);
|
||||||
|
return new CompilationResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
ErrorMessage = "Compilation failed",
|
||||||
|
CompilerOutput = compilerOutput
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error during Kotlin compilation");
|
||||||
|
return new CompilationResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
ErrorMessage = $"Compilation error: {ex.Message}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private (string compiler, string compilerFlags) ResolveVersion(string? version)
|
||||||
|
{
|
||||||
|
// If version is null or "latest", use default configuration
|
||||||
|
if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var compiler = _configuration["Kotlin:Compiler"] ?? "kotlinc";
|
||||||
|
var compilerFlags = _configuration["Kotlin:CompilerFlags"] ?? "";
|
||||||
|
return (compiler, compilerFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find version-specific configuration
|
||||||
|
var versionKey = $"Kotlin:Versions:{version}";
|
||||||
|
var versionCompiler = _configuration[$"{versionKey}:Compiler"];
|
||||||
|
var versionFlags = _configuration[$"{versionKey}:CompilerFlags"];
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(versionCompiler))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Using Kotlin version {Version} configuration", version);
|
||||||
|
return (versionCompiler, versionFlags ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version not found, use default and log warning
|
||||||
|
_logger.LogWarning("Kotlin version {Version} not found in configuration, using default", version);
|
||||||
|
var defaultCompiler = _configuration["Kotlin:Compiler"] ?? "kotlinc";
|
||||||
|
var defaultFlags = _configuration["Kotlin:CompilerFlags"] ?? "";
|
||||||
|
return (defaultCompiler, defaultFlags);
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/LiquidCode.Tester.Worker/Services/KotlinExecutionService.cs
Normal file
110
src/LiquidCode.Tester.Worker/Services/KotlinExecutionService.cs
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
|
|
||||||
|
public class KotlinExecutionService : IExecutionService
|
||||||
|
{
|
||||||
|
private readonly ILogger<KotlinExecutionService> _logger;
|
||||||
|
|
||||||
|
public KotlinExecutionService(ILogger<KotlinExecutionService> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ExecutionResult> ExecuteAsync(string executablePath, string inputFilePath, int timeLimitMs, int memoryLimitMb)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Executing Kotlin JAR {Executable} with input {Input}, time limit {TimeLimit}ms, memory limit {MemoryLimit}MB",
|
||||||
|
executablePath, inputFilePath, timeLimitMs, memoryLimitMb);
|
||||||
|
|
||||||
|
var result = new ExecutionResult();
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var inputStream = File.OpenRead(inputFilePath);
|
||||||
|
|
||||||
|
var process = new Process
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "java",
|
||||||
|
Arguments = $"-jar \"{executablePath}\"",
|
||||||
|
WorkingDirectory = Path.GetDirectoryName(executablePath),
|
||||||
|
RedirectStandardInput = true,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
|
||||||
|
await inputStream.CopyToAsync(process.StandardInput.BaseStream);
|
||||||
|
process.StandardInput.Close();
|
||||||
|
|
||||||
|
var outputTask = process.StandardOutput.ReadToEndAsync();
|
||||||
|
var errorTask = process.StandardError.ReadToEndAsync();
|
||||||
|
|
||||||
|
var completedInTime = await Task.Run(() => process.WaitForExit(timeLimitMs));
|
||||||
|
|
||||||
|
stopwatch.Stop();
|
||||||
|
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
if (!completedInTime)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
process.Kill(entireProcessTree: true);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
result.TimeLimitExceeded = true;
|
||||||
|
result.ErrorMessage = "Time limit exceeded";
|
||||||
|
_logger.LogWarning("Execution exceeded time limit");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Output = await outputTask;
|
||||||
|
result.ErrorOutput = await errorTask;
|
||||||
|
result.ExitCode = process.ExitCode;
|
||||||
|
|
||||||
|
if (process.ExitCode != 0)
|
||||||
|
{
|
||||||
|
result.RuntimeError = true;
|
||||||
|
result.ErrorMessage = $"Runtime error (exit code {process.ExitCode})";
|
||||||
|
_logger.LogWarning("Runtime error with exit code {ExitCode}", process.ExitCode);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result.MemoryUsedMb = process.PeakWorkingSet64 / (1024 * 1024);
|
||||||
|
if (result.MemoryUsedMb > memoryLimitMb)
|
||||||
|
{
|
||||||
|
result.MemoryLimitExceeded = true;
|
||||||
|
result.ErrorMessage = "Memory limit exceeded";
|
||||||
|
_logger.LogWarning("Memory limit exceeded: {Used}MB > {Limit}MB", result.MemoryUsedMb, memoryLimitMb);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Could not measure memory usage");
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Success = true;
|
||||||
|
_logger.LogInformation("Execution completed successfully in {Time}ms", result.ExecutionTimeMs);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
|
||||||
|
result.RuntimeError = true;
|
||||||
|
result.ErrorMessage = $"Execution error: {ex.Message}";
|
||||||
|
_logger.LogError(ex, "Error during execution");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using LiquidCode.Tester.Worker.Models;
|
||||||
|
|
||||||
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
|
|
||||||
|
public class PythonCompilationService : ICompilationService
|
||||||
|
{
|
||||||
|
private readonly ILogger<PythonCompilationService> _logger;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
public PythonCompilationService(ILogger<PythonCompilationService> logger, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CompilationResult> CompileAsync(string sourceCode, string workingDirectory, string? version = null)
|
||||||
|
{
|
||||||
|
var sourceFilePath = Path.Combine(workingDirectory, "solution.py");
|
||||||
|
|
||||||
|
_logger.LogInformation("Preparing Python code in {WorkingDirectory} with version {Version}", workingDirectory, version ?? "latest");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await File.WriteAllTextAsync(sourceFilePath, sourceCode);
|
||||||
|
|
||||||
|
// Resolve version-specific executable (for execution service)
|
||||||
|
var executable = ResolveVersion(version);
|
||||||
|
|
||||||
|
_logger.LogDebug("Using Python executable: {Executable}", executable);
|
||||||
|
_logger.LogInformation("Python code prepared successfully (no compilation needed)");
|
||||||
|
|
||||||
|
return new CompilationResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
ExecutablePath = sourceFilePath,
|
||||||
|
CompilerOutput = $"Python is an interpreted language - no compilation needed (using {executable})"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error preparing Python code");
|
||||||
|
return new CompilationResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
ErrorMessage = $"Error preparing code: {ex.Message}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveVersion(string? version)
|
||||||
|
{
|
||||||
|
// If version is null or "latest", use default configuration
|
||||||
|
if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var executable = _configuration["Python:Executable"] ?? "python3";
|
||||||
|
return executable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find version-specific configuration
|
||||||
|
var versionKey = $"Python:Versions:{version}";
|
||||||
|
var versionExecutable = _configuration[$"{versionKey}:Executable"];
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(versionExecutable))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Using Python version {Version} configuration", version);
|
||||||
|
return versionExecutable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version not found, use default and log warning
|
||||||
|
_logger.LogWarning("Python version {Version} not found in configuration, using default", version);
|
||||||
|
var defaultExecutable = _configuration["Python:Executable"] ?? "python3";
|
||||||
|
return defaultExecutable;
|
||||||
|
}
|
||||||
|
}
|
||||||
114
src/LiquidCode.Tester.Worker/Services/PythonExecutionService.cs
Normal file
114
src/LiquidCode.Tester.Worker/Services/PythonExecutionService.cs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
|
|
||||||
|
public class PythonExecutionService : IExecutionService
|
||||||
|
{
|
||||||
|
private readonly ILogger<PythonExecutionService> _logger;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
public PythonExecutionService(ILogger<PythonExecutionService> logger, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ExecutionResult> ExecuteAsync(string executablePath, string inputFilePath, int timeLimitMs, int memoryLimitMb)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Executing Python script {Executable} with input {Input}, time limit {TimeLimit}ms, memory limit {MemoryLimit}MB",
|
||||||
|
executablePath, inputFilePath, timeLimitMs, memoryLimitMb);
|
||||||
|
|
||||||
|
var result = new ExecutionResult();
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var inputStream = File.OpenRead(inputFilePath);
|
||||||
|
|
||||||
|
var pythonExecutable = _configuration["Python:Executable"] ?? "python3";
|
||||||
|
|
||||||
|
var process = new Process
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = pythonExecutable,
|
||||||
|
Arguments = $"\"{executablePath}\"",
|
||||||
|
WorkingDirectory = Path.GetDirectoryName(executablePath),
|
||||||
|
RedirectStandardInput = true,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
|
||||||
|
await inputStream.CopyToAsync(process.StandardInput.BaseStream);
|
||||||
|
process.StandardInput.Close();
|
||||||
|
|
||||||
|
var outputTask = process.StandardOutput.ReadToEndAsync();
|
||||||
|
var errorTask = process.StandardError.ReadToEndAsync();
|
||||||
|
|
||||||
|
var completedInTime = await Task.Run(() => process.WaitForExit(timeLimitMs));
|
||||||
|
|
||||||
|
stopwatch.Stop();
|
||||||
|
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
if (!completedInTime)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
process.Kill(entireProcessTree: true);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
result.TimeLimitExceeded = true;
|
||||||
|
result.ErrorMessage = "Time limit exceeded";
|
||||||
|
_logger.LogWarning("Execution exceeded time limit");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Output = await outputTask;
|
||||||
|
result.ErrorOutput = await errorTask;
|
||||||
|
result.ExitCode = process.ExitCode;
|
||||||
|
|
||||||
|
if (process.ExitCode != 0)
|
||||||
|
{
|
||||||
|
result.RuntimeError = true;
|
||||||
|
result.ErrorMessage = $"Runtime error (exit code {process.ExitCode})";
|
||||||
|
_logger.LogWarning("Runtime error with exit code {ExitCode}", process.ExitCode);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result.MemoryUsedMb = process.PeakWorkingSet64 / (1024 * 1024);
|
||||||
|
if (result.MemoryUsedMb > memoryLimitMb)
|
||||||
|
{
|
||||||
|
result.MemoryLimitExceeded = true;
|
||||||
|
result.ErrorMessage = "Memory limit exceeded";
|
||||||
|
_logger.LogWarning("Memory limit exceeded: {Used}MB > {Limit}MB", result.MemoryUsedMb, memoryLimitMb);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Could not measure memory usage");
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Success = true;
|
||||||
|
_logger.LogInformation("Execution completed successfully in {Time}ms", result.ExecutionTimeMs);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
|
||||||
|
result.RuntimeError = true;
|
||||||
|
result.ErrorMessage = $"Execution error: {ex.Message}";
|
||||||
|
_logger.LogError(ex, "Error during execution");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,23 +6,23 @@ namespace LiquidCode.Tester.Worker.Services;
|
|||||||
public class TestingService : ITestingService
|
public class TestingService : ITestingService
|
||||||
{
|
{
|
||||||
private readonly IPackageParserService _packageParser;
|
private readonly IPackageParserService _packageParser;
|
||||||
private readonly ICompilationService _compilationService;
|
private readonly ICompilationServiceFactory _compilationServiceFactory;
|
||||||
private readonly IExecutionService _executionService;
|
private readonly IExecutionServiceFactory _executionServiceFactory;
|
||||||
private readonly IOutputCheckerService _outputChecker;
|
private readonly IOutputCheckerService _outputChecker;
|
||||||
private readonly ICallbackService _callbackService;
|
private readonly ICallbackService _callbackService;
|
||||||
private readonly ILogger<TestingService> _logger;
|
private readonly ILogger<TestingService> _logger;
|
||||||
|
|
||||||
public TestingService(
|
public TestingService(
|
||||||
IPackageParserService packageParser,
|
IPackageParserService packageParser,
|
||||||
ICompilationService compilationService,
|
ICompilationServiceFactory compilationServiceFactory,
|
||||||
IExecutionService executionService,
|
IExecutionServiceFactory executionServiceFactory,
|
||||||
IOutputCheckerService outputChecker,
|
IOutputCheckerService outputChecker,
|
||||||
ICallbackService callbackService,
|
ICallbackService callbackService,
|
||||||
ILogger<TestingService> logger)
|
ILogger<TestingService> logger)
|
||||||
{
|
{
|
||||||
_packageParser = packageParser;
|
_packageParser = packageParser;
|
||||||
_compilationService = compilationService;
|
_compilationServiceFactory = compilationServiceFactory;
|
||||||
_executionService = executionService;
|
_executionServiceFactory = executionServiceFactory;
|
||||||
_outputChecker = outputChecker;
|
_outputChecker = outputChecker;
|
||||||
_callbackService = callbackService;
|
_callbackService = callbackService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -39,8 +39,15 @@ public class TestingService : ITestingService
|
|||||||
|
|
||||||
// Parse package
|
// Parse package
|
||||||
ProblemPackage package;
|
ProblemPackage package;
|
||||||
if (request.Package != null)
|
if (!string.IsNullOrEmpty(request.PackageFilePath))
|
||||||
{
|
{
|
||||||
|
// Use saved file path (from background task)
|
||||||
|
await using var fileStream = File.OpenRead(request.PackageFilePath);
|
||||||
|
package = await _packageParser.ParsePackageAsync(fileStream);
|
||||||
|
}
|
||||||
|
else if (request.Package != null)
|
||||||
|
{
|
||||||
|
// Use IFormFile directly (should not happen in background tasks)
|
||||||
using var packageStream = request.Package.OpenReadStream();
|
using var packageStream = request.Package.OpenReadStream();
|
||||||
package = await _packageParser.ParsePackageAsync(packageStream);
|
package = await _packageParser.ParsePackageAsync(packageStream);
|
||||||
}
|
}
|
||||||
@@ -54,8 +61,12 @@ public class TestingService : ITestingService
|
|||||||
// Send compiling status
|
// Send compiling status
|
||||||
await SendStatusAsync(request, State.Compiling, ErrorCode.None, "Compiling solution", 0, package.TestCases.Count);
|
await SendStatusAsync(request, State.Compiling, ErrorCode.None, "Compiling solution", 0, package.TestCases.Count);
|
||||||
|
|
||||||
|
// Get language-specific services
|
||||||
|
var compilationService = _compilationServiceFactory.GetCompilationService(request.Language);
|
||||||
|
var executionService = _executionServiceFactory.GetExecutionService(request.Language);
|
||||||
|
|
||||||
// Compile user solution
|
// Compile user solution
|
||||||
var compilationResult = await _compilationService.CompileAsync(request.SourceCode, package.WorkingDirectory);
|
var compilationResult = await compilationService.CompileAsync(request.SourceCode, package.WorkingDirectory, request.LanguageVersion);
|
||||||
|
|
||||||
if (!compilationResult.Success)
|
if (!compilationResult.Success)
|
||||||
{
|
{
|
||||||
@@ -80,7 +91,7 @@ public class TestingService : ITestingService
|
|||||||
$"Running test {testCase.Number}", testCase.Number, package.TestCases.Count);
|
$"Running test {testCase.Number}", testCase.Number, package.TestCases.Count);
|
||||||
|
|
||||||
// Execute solution
|
// Execute solution
|
||||||
var executionResult = await _executionService.ExecuteAsync(
|
var executionResult = await executionService.ExecuteAsync(
|
||||||
compilationResult.ExecutablePath!,
|
compilationResult.ExecutablePath!,
|
||||||
testCase.InputFilePath,
|
testCase.InputFilePath,
|
||||||
testCase.TimeLimit,
|
testCase.TimeLimit,
|
||||||
|
|||||||
@@ -7,7 +7,84 @@
|
|||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"Cpp": {
|
"Cpp": {
|
||||||
|
"Compiler": "g++",
|
||||||
|
"CompilerFlags": "-O2 -std=c++17 -Wall",
|
||||||
|
"Versions": {
|
||||||
|
"14": {
|
||||||
|
"Compiler": "g++",
|
||||||
|
"CompilerFlags": "-O2 -std=c++14 -Wall"
|
||||||
|
},
|
||||||
|
"17": {
|
||||||
"Compiler": "g++",
|
"Compiler": "g++",
|
||||||
"CompilerFlags": "-O2 -std=c++17 -Wall"
|
"CompilerFlags": "-O2 -std=c++17 -Wall"
|
||||||
|
},
|
||||||
|
"20": {
|
||||||
|
"Compiler": "g++",
|
||||||
|
"CompilerFlags": "-O2 -std=c++20 -Wall"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Java": {
|
||||||
|
"Compiler": "javac",
|
||||||
|
"CompilerFlags": "",
|
||||||
|
"Versions": {
|
||||||
|
"8": {
|
||||||
|
"Compiler": "javac",
|
||||||
|
"CompilerFlags": "-source 8 -target 8"
|
||||||
|
},
|
||||||
|
"11": {
|
||||||
|
"Compiler": "javac",
|
||||||
|
"CompilerFlags": "-source 11 -target 11"
|
||||||
|
},
|
||||||
|
"17": {
|
||||||
|
"Compiler": "javac",
|
||||||
|
"CompilerFlags": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Kotlin": {
|
||||||
|
"Compiler": "kotlinc",
|
||||||
|
"CompilerFlags": "",
|
||||||
|
"Versions": {
|
||||||
|
"1.9": {
|
||||||
|
"Compiler": "kotlinc",
|
||||||
|
"CompilerFlags": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CSharp": {
|
||||||
|
"Compiler": "csc",
|
||||||
|
"CompilerFlags": "/optimize+",
|
||||||
|
"Versions": {
|
||||||
|
"7": {
|
||||||
|
"Compiler": "csc",
|
||||||
|
"CompilerFlags": "/optimize+ /langversion:7"
|
||||||
|
},
|
||||||
|
"8": {
|
||||||
|
"Compiler": "csc",
|
||||||
|
"CompilerFlags": "/optimize+ /langversion:8"
|
||||||
|
},
|
||||||
|
"9": {
|
||||||
|
"Compiler": "csc",
|
||||||
|
"CompilerFlags": "/optimize+ /langversion:9"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Python": {
|
||||||
|
"Executable": "python3",
|
||||||
|
"Versions": {
|
||||||
|
"3.8": {
|
||||||
|
"Executable": "python3.8"
|
||||||
|
},
|
||||||
|
"3.9": {
|
||||||
|
"Executable": "python3.9"
|
||||||
|
},
|
||||||
|
"3.10": {
|
||||||
|
"Executable": "python3.10"
|
||||||
|
},
|
||||||
|
"3.11": {
|
||||||
|
"Executable": "python3.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
using LiquidCode.Tester.Worker.Services;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Moq;
|
||||||
|
|
||||||
|
namespace LiquidCode.Tester.Worker.Tests;
|
||||||
|
|
||||||
|
public class CompilationServiceFactoryTests
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly CompilationServiceFactory _factory;
|
||||||
|
|
||||||
|
public CompilationServiceFactoryTests()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
// Mock configuration
|
||||||
|
var configuration = new Mock<IConfiguration>();
|
||||||
|
services.AddSingleton(configuration.Object);
|
||||||
|
|
||||||
|
// Mock logger
|
||||||
|
var loggerFactory = new Mock<ILoggerFactory>();
|
||||||
|
loggerFactory.Setup(x => x.CreateLogger(It.IsAny<string>()))
|
||||||
|
.Returns(new Mock<ILogger>().Object);
|
||||||
|
services.AddSingleton(loggerFactory.Object);
|
||||||
|
services.AddLogging();
|
||||||
|
|
||||||
|
// Register compilation services
|
||||||
|
services.AddSingleton<CppCompilationService>();
|
||||||
|
services.AddSingleton<JavaCompilationService>();
|
||||||
|
services.AddSingleton<KotlinCompilationService>();
|
||||||
|
services.AddSingleton<CSharpCompilationService>();
|
||||||
|
services.AddSingleton<PythonCompilationService>();
|
||||||
|
|
||||||
|
_serviceProvider = services.BuildServiceProvider();
|
||||||
|
_factory = new CompilationServiceFactory(
|
||||||
|
_serviceProvider,
|
||||||
|
_serviceProvider.GetRequiredService<ILogger<CompilationServiceFactory>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("C++", typeof(CppCompilationService))]
|
||||||
|
[InlineData("c++", typeof(CppCompilationService))]
|
||||||
|
[InlineData("cpp", typeof(CppCompilationService))]
|
||||||
|
[InlineData("CPP", typeof(CppCompilationService))]
|
||||||
|
public void GetCompilationService_CppLanguage_ReturnsCppCompilationService(string language, Type expectedType)
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var service = _factory.GetCompilationService(language);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType(expectedType, service);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Java", typeof(JavaCompilationService))]
|
||||||
|
[InlineData("java", typeof(JavaCompilationService))]
|
||||||
|
[InlineData("JAVA", typeof(JavaCompilationService))]
|
||||||
|
public void GetCompilationService_JavaLanguage_ReturnsJavaCompilationService(string language, Type expectedType)
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var service = _factory.GetCompilationService(language);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType(expectedType, service);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Kotlin", typeof(KotlinCompilationService))]
|
||||||
|
[InlineData("kotlin", typeof(KotlinCompilationService))]
|
||||||
|
[InlineData("KOTLIN", typeof(KotlinCompilationService))]
|
||||||
|
public void GetCompilationService_KotlinLanguage_ReturnsKotlinCompilationService(string language, Type expectedType)
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var service = _factory.GetCompilationService(language);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType(expectedType, service);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("C#", typeof(CSharpCompilationService))]
|
||||||
|
[InlineData("c#", typeof(CSharpCompilationService))]
|
||||||
|
[InlineData("csharp", typeof(CSharpCompilationService))]
|
||||||
|
[InlineData("CSharp", typeof(CSharpCompilationService))]
|
||||||
|
[InlineData("CSHARP", typeof(CSharpCompilationService))]
|
||||||
|
public void GetCompilationService_CSharpLanguage_ReturnsCSharpCompilationService(string language, Type expectedType)
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var service = _factory.GetCompilationService(language);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType(expectedType, service);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Python", typeof(PythonCompilationService))]
|
||||||
|
[InlineData("python", typeof(PythonCompilationService))]
|
||||||
|
[InlineData("PYTHON", typeof(PythonCompilationService))]
|
||||||
|
public void GetCompilationService_PythonLanguage_ReturnsPythonCompilationService(string language, Type expectedType)
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var service = _factory.GetCompilationService(language);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType(expectedType, service);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Go")]
|
||||||
|
[InlineData("Rust")]
|
||||||
|
[InlineData("JavaScript")]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData(" ")]
|
||||||
|
public void GetCompilationService_UnsupportedLanguage_ThrowsNotSupportedException(string language)
|
||||||
|
{
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<NotSupportedException>(() => _factory.GetCompilationService(language));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCompilationService_NullLanguage_ThrowsException()
|
||||||
|
{
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<NullReferenceException>(() => _factory.GetCompilationService(null!));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
using LiquidCode.Tester.Worker.Services;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Moq;
|
||||||
|
|
||||||
|
namespace LiquidCode.Tester.Worker.Tests;
|
||||||
|
|
||||||
|
public class ExecutionServiceFactoryTests
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly ExecutionServiceFactory _factory;
|
||||||
|
|
||||||
|
public ExecutionServiceFactoryTests()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
// Mock configuration
|
||||||
|
var configuration = new Mock<IConfiguration>();
|
||||||
|
services.AddSingleton(configuration.Object);
|
||||||
|
|
||||||
|
// Mock logger
|
||||||
|
var loggerFactory = new Mock<ILoggerFactory>();
|
||||||
|
loggerFactory.Setup(x => x.CreateLogger(It.IsAny<string>()))
|
||||||
|
.Returns(new Mock<ILogger>().Object);
|
||||||
|
services.AddSingleton(loggerFactory.Object);
|
||||||
|
services.AddLogging();
|
||||||
|
|
||||||
|
// Register execution services
|
||||||
|
services.AddSingleton<CppExecutionService>();
|
||||||
|
services.AddSingleton<JavaExecutionService>();
|
||||||
|
services.AddSingleton<KotlinExecutionService>();
|
||||||
|
services.AddSingleton<CSharpExecutionService>();
|
||||||
|
services.AddSingleton<PythonExecutionService>();
|
||||||
|
|
||||||
|
_serviceProvider = services.BuildServiceProvider();
|
||||||
|
_factory = new ExecutionServiceFactory(
|
||||||
|
_serviceProvider,
|
||||||
|
_serviceProvider.GetRequiredService<ILogger<ExecutionServiceFactory>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("C++", typeof(CppExecutionService))]
|
||||||
|
[InlineData("c++", typeof(CppExecutionService))]
|
||||||
|
[InlineData("cpp", typeof(CppExecutionService))]
|
||||||
|
[InlineData("CPP", typeof(CppExecutionService))]
|
||||||
|
public void GetExecutionService_CppLanguage_ReturnsCppExecutionService(string language, Type expectedType)
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var service = _factory.GetExecutionService(language);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType(expectedType, service);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Java", typeof(JavaExecutionService))]
|
||||||
|
[InlineData("java", typeof(JavaExecutionService))]
|
||||||
|
[InlineData("JAVA", typeof(JavaExecutionService))]
|
||||||
|
public void GetExecutionService_JavaLanguage_ReturnsJavaExecutionService(string language, Type expectedType)
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var service = _factory.GetExecutionService(language);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType(expectedType, service);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Kotlin", typeof(KotlinExecutionService))]
|
||||||
|
[InlineData("kotlin", typeof(KotlinExecutionService))]
|
||||||
|
[InlineData("KOTLIN", typeof(KotlinExecutionService))]
|
||||||
|
public void GetExecutionService_KotlinLanguage_ReturnsKotlinExecutionService(string language, Type expectedType)
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var service = _factory.GetExecutionService(language);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType(expectedType, service);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("C#", typeof(CSharpExecutionService))]
|
||||||
|
[InlineData("c#", typeof(CSharpExecutionService))]
|
||||||
|
[InlineData("csharp", typeof(CSharpExecutionService))]
|
||||||
|
[InlineData("CSharp", typeof(CSharpExecutionService))]
|
||||||
|
[InlineData("CSHARP", typeof(CSharpExecutionService))]
|
||||||
|
public void GetExecutionService_CSharpLanguage_ReturnsCSharpExecutionService(string language, Type expectedType)
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var service = _factory.GetExecutionService(language);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType(expectedType, service);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Python", typeof(PythonExecutionService))]
|
||||||
|
[InlineData("python", typeof(PythonExecutionService))]
|
||||||
|
[InlineData("PYTHON", typeof(PythonExecutionService))]
|
||||||
|
public void GetExecutionService_PythonLanguage_ReturnsPythonExecutionService(string language, Type expectedType)
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var service = _factory.GetExecutionService(language);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType(expectedType, service);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Go")]
|
||||||
|
[InlineData("Rust")]
|
||||||
|
[InlineData("JavaScript")]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData(" ")]
|
||||||
|
public void GetExecutionService_UnsupportedLanguage_ThrowsNotSupportedException(string language)
|
||||||
|
{
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<NotSupportedException>(() => _factory.GetExecutionService(language));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetExecutionService_NullLanguage_ThrowsException()
|
||||||
|
{
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<NullReferenceException>(() => _factory.GetExecutionService(null!));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||||
|
<PackageReference Include="Moq" Version="4.20.70" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\LiquidCode.Tester.Worker\LiquidCode.Tester.Worker.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\LiquidCode.Tester.Common\LiquidCode.Tester.Common.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
using LiquidCode.Tester.Worker.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Moq;
|
||||||
|
|
||||||
|
namespace LiquidCode.Tester.Worker.Tests;
|
||||||
|
|
||||||
|
public class OutputCheckerServiceTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly OutputCheckerService _service;
|
||||||
|
private readonly string _testDirectory;
|
||||||
|
|
||||||
|
public OutputCheckerServiceTests()
|
||||||
|
{
|
||||||
|
var logger = new Mock<ILogger<OutputCheckerService>>();
|
||||||
|
_service = new OutputCheckerService(logger.Object);
|
||||||
|
_testDirectory = Path.Combine(Path.GetTempPath(), "OutputCheckerTests", Guid.NewGuid().ToString());
|
||||||
|
Directory.CreateDirectory(_testDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CheckOutputAsync_ExactMatch_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedOutput = "Hello, World!";
|
||||||
|
var actualOutput = "Hello, World!";
|
||||||
|
var expectedFilePath = CreateTempFile(expectedOutput);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.CheckOutputAsync(actualOutput, expectedFilePath);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CheckOutputAsync_DifferentOutput_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedOutput = "Hello, World!";
|
||||||
|
var actualOutput = "Goodbye, World!";
|
||||||
|
var expectedFilePath = CreateTempFile(expectedOutput);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.CheckOutputAsync(actualOutput, expectedFilePath);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CheckOutputAsync_TrailingWhitespace_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedOutput = "Hello, World!";
|
||||||
|
var actualOutput = "Hello, World! \n\n";
|
||||||
|
var expectedFilePath = CreateTempFile(expectedOutput);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.CheckOutputAsync(actualOutput, expectedFilePath);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CheckOutputAsync_LeadingWhitespace_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedOutput = "Hello, World!";
|
||||||
|
var actualOutput = " \n\nHello, World!";
|
||||||
|
var expectedFilePath = CreateTempFile(expectedOutput);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.CheckOutputAsync(actualOutput, expectedFilePath);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CheckOutputAsync_DifferentLineEndings_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedOutput = "Line1\nLine2\nLine3";
|
||||||
|
var actualOutput = "Line1\r\nLine2\r\nLine3";
|
||||||
|
var expectedFilePath = CreateTempFile(expectedOutput);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.CheckOutputAsync(actualOutput, expectedFilePath);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CheckOutputAsync_MultipleLines_ExactMatch_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedOutput = "Line 1\nLine 2\nLine 3";
|
||||||
|
var actualOutput = "Line 1\nLine 2\nLine 3";
|
||||||
|
var expectedFilePath = CreateTempFile(expectedOutput);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.CheckOutputAsync(actualOutput, expectedFilePath);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CheckOutputAsync_EmptyOutputs_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedOutput = "";
|
||||||
|
var actualOutput = "";
|
||||||
|
var expectedFilePath = CreateTempFile(expectedOutput);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.CheckOutputAsync(actualOutput, expectedFilePath);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CheckOutputAsync_OnlyWhitespace_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedOutput = " \n\n ";
|
||||||
|
var actualOutput = " \n\n ";
|
||||||
|
var expectedFilePath = CreateTempFile(expectedOutput);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.CheckOutputAsync(actualOutput, expectedFilePath);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CheckOutputAsync_CaseSensitive_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedOutput = "Hello, World!";
|
||||||
|
var actualOutput = "hello, world!";
|
||||||
|
var expectedFilePath = CreateTempFile(expectedOutput);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.CheckOutputAsync(actualOutput, expectedFilePath);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string CreateTempFile(string content)
|
||||||
|
{
|
||||||
|
var filePath = Path.Combine(_testDirectory, $"test_{Guid.NewGuid()}.txt");
|
||||||
|
File.WriteAllText(filePath, content);
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Directory.Exists(_testDirectory))
|
||||||
|
{
|
||||||
|
Directory.Delete(_testDirectory, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
using System.IO.Compression;
|
||||||
|
using LiquidCode.Tester.Worker.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Moq;
|
||||||
|
|
||||||
|
namespace LiquidCode.Tester.Worker.Tests;
|
||||||
|
|
||||||
|
public class PackageParserServiceTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly PackageParserService _service;
|
||||||
|
private readonly string _testDirectory;
|
||||||
|
|
||||||
|
public PackageParserServiceTests()
|
||||||
|
{
|
||||||
|
var logger = new Mock<ILogger<PackageParserService>>();
|
||||||
|
_service = new PackageParserService(logger.Object);
|
||||||
|
_testDirectory = Path.Combine(Path.GetTempPath(), "PackageParserTests", Guid.NewGuid().ToString());
|
||||||
|
Directory.CreateDirectory(_testDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ParsePackageAsync_ValidPackageWithTestCases_ParsesSuccessfully()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var zipStream = CreateTestPackage(new[]
|
||||||
|
{
|
||||||
|
("1.in", "input1"),
|
||||||
|
("1.out", "output1"),
|
||||||
|
("2.in", "input2"),
|
||||||
|
("2.out", "output2"),
|
||||||
|
("3.in", "input3"),
|
||||||
|
("3.out", "output3")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.ParsePackageAsync(zipStream);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(3, result.TestCases.Count);
|
||||||
|
Assert.True(Directory.Exists(result.WorkingDirectory));
|
||||||
|
|
||||||
|
// Check test case 1
|
||||||
|
Assert.Equal(1, result.TestCases[0].Number);
|
||||||
|
Assert.True(File.Exists(result.TestCases[0].InputFilePath));
|
||||||
|
Assert.True(File.Exists(result.TestCases[0].OutputFilePath));
|
||||||
|
Assert.Equal("input1", await File.ReadAllTextAsync(result.TestCases[0].InputFilePath));
|
||||||
|
|
||||||
|
// Check test case 2
|
||||||
|
Assert.Equal(2, result.TestCases[1].Number);
|
||||||
|
Assert.True(File.Exists(result.TestCases[1].InputFilePath));
|
||||||
|
Assert.True(File.Exists(result.TestCases[1].OutputFilePath));
|
||||||
|
|
||||||
|
// Check test case 3
|
||||||
|
Assert.Equal(3, result.TestCases[2].Number);
|
||||||
|
Assert.True(File.Exists(result.TestCases[2].InputFilePath));
|
||||||
|
Assert.True(File.Exists(result.TestCases[2].OutputFilePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ParsePackageAsync_PackageWithDefaultLimits_UsesDefaultValues()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var zipStream = CreateTestPackage(new[]
|
||||||
|
{
|
||||||
|
("1.in", "input1"),
|
||||||
|
("1.out", "output1")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.ParsePackageAsync(zipStream);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(result.TestCases);
|
||||||
|
Assert.Equal(2000, result.TestCases[0].TimeLimit); // Default time limit
|
||||||
|
Assert.Equal(256, result.TestCases[0].MemoryLimit); // Default memory limit
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ParsePackageAsync_EmptyPackage_ReturnsEmptyTestCasesList()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var zipStream = CreateTestPackage(Array.Empty<(string, string)>());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.ParsePackageAsync(zipStream);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Empty(result.TestCases);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ParsePackageAsync_TestCasesNotInOrder_SortsCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var zipStream = CreateTestPackage(new[]
|
||||||
|
{
|
||||||
|
("3.in", "input3"),
|
||||||
|
("3.out", "output3"),
|
||||||
|
("1.in", "input1"),
|
||||||
|
("1.out", "output1"),
|
||||||
|
("2.in", "input2"),
|
||||||
|
("2.out", "output2")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.ParsePackageAsync(zipStream);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(3, result.TestCases.Count);
|
||||||
|
Assert.Equal(1, result.TestCases[0].Number);
|
||||||
|
Assert.Equal(2, result.TestCases[1].Number);
|
||||||
|
Assert.Equal(3, result.TestCases[2].Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ParsePackageAsync_MissingOutputFile_SkipsTestCase()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var zipStream = CreateTestPackage(new[]
|
||||||
|
{
|
||||||
|
("1.in", "input1"),
|
||||||
|
("1.out", "output1"),
|
||||||
|
("2.in", "input2"),
|
||||||
|
// Missing 2.out
|
||||||
|
("3.in", "input3"),
|
||||||
|
("3.out", "output3")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.ParsePackageAsync(zipStream);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result.TestCases.Count); // Only test 1 and 3
|
||||||
|
Assert.Equal(1, result.TestCases[0].Number);
|
||||||
|
Assert.Equal(3, result.TestCases[1].Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ParsePackageAsync_MissingInputFile_SkipsTestCase()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var zipStream = CreateTestPackage(new[]
|
||||||
|
{
|
||||||
|
("1.in", "input1"),
|
||||||
|
("1.out", "output1"),
|
||||||
|
// Missing 2.in
|
||||||
|
("2.out", "output2"),
|
||||||
|
("3.in", "input3"),
|
||||||
|
("3.out", "output3")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.ParsePackageAsync(zipStream);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result.TestCases.Count); // Only test 1 and 3
|
||||||
|
Assert.Equal(1, result.TestCases[0].Number);
|
||||||
|
Assert.Equal(3, result.TestCases[1].Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ParsePackageAsync_TestsInSubdirectory_ParsesSuccessfully()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var zipStream = CreateTestPackage(new[]
|
||||||
|
{
|
||||||
|
("tests/1.in", "input1"),
|
||||||
|
("tests/1.out", "output1"),
|
||||||
|
("tests/2.in", "input2"),
|
||||||
|
("tests/2.out", "output2")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.ParsePackageAsync(zipStream);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result.TestCases.Count);
|
||||||
|
Assert.Equal(1, result.TestCases[0].Number);
|
||||||
|
Assert.Equal(2, result.TestCases[1].Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MemoryStream CreateTestPackage(IEnumerable<(string fileName, string content)> files)
|
||||||
|
{
|
||||||
|
var memoryStream = new MemoryStream();
|
||||||
|
|
||||||
|
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
|
||||||
|
{
|
||||||
|
foreach (var (fileName, content) in files)
|
||||||
|
{
|
||||||
|
var entry = archive.CreateEntry(fileName);
|
||||||
|
using var entryStream = entry.Open();
|
||||||
|
using var writer = new StreamWriter(entryStream);
|
||||||
|
writer.Write(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
memoryStream.Position = 0;
|
||||||
|
return memoryStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Directory.Exists(_testDirectory))
|
||||||
|
{
|
||||||
|
Directory.Delete(_testDirectory, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user