Roman Pytkov bf7bd0ad6b
All checks were successful
Build and Push Docker Images / build (src/LiquidCode.Tester.Gateway/Dockerfile, git.nullptr.top/liquidcode/liquidcode-tester-gateway-roman, gateway) (push) Successful in 53s
Build and Push Docker Images / build (src/LiquidCode.Tester.Worker/Dockerfile, git.nullptr.top/liquidcode/liquidcode-tester-worker-roman, worker) (push) Successful in 50s
Improves package download and caching
Adds package caching to reduce download frequency.

Introduces a `PackageDownloadService` with memory caching to store downloaded packages,
identified by mission ID, for reuse. Uses concurrent locks to prevent race conditions during download.

Modifies the worker client service to optionally delete the package after sending,
allowing cached packages to be retained.
2025-11-02 20:05:50 +03:00
2025-11-02 19:31:34 +03:00
2025-11-02 20:05:50 +03:00
2025-10-24 23:19:00 +04:00
2025-10-24 23:55:10 +04:00
2025-10-24 23:19:00 +04:00

LiquidCode Tester

Система автоматической проверки решений задач по программированию (Online Judge) для платформы LiquidCode.

Архитектура

Проект состоит из трёх основных компонентов:

1. Gateway (Шлюз)

  • Технология: ASP.NET Core Web API
  • Функции:
    • Приём запросов на тестирование через REST API
    • Скачивание пакетов задач из Polygon
    • Маршрутизация запросов к соответствующим Worker'ам по языку программирования

2. Worker (Универсальный тестировщик)

  • Технология: ASP.NET Core Web API + компиляторы всех поддерживаемых языков
  • Функции:
    • Динамический выбор компилятора/интерпретатора на основе языка
    • Компиляция пользовательского кода (C++, Java, Kotlin, C#) или подготовка к исполнению (Python)
    • Запуск в изолированной среде
    • Тестирование на наборе тестов
    • Контроль ограничений по времени и памяти
    • Отправка статусов на callback URL

3. Common (Общая библиотека)

  • Технология: .NET Class Library
  • Функции: Общие модели данных для Gateway и Worker

Поддерживаемые языки

  • C++ (реализовано)
  • Java (реализовано)
  • Kotlin (реализовано)
  • C# (реализовано)
  • Python (реализовано)

Модель данных

SubmitForTesterModel (запрос на тестирование)

public record SubmitForTesterModel(
    long Id,
    long MissionId,
    string Language,
    string LanguageVersion,
    string SourceCode,
    string PackageUrl,
    string CallbackUrl
);

TesterResponseModel (результат тестирования)

public record TesterResponseModel(
    long SubmitId,
    State State,          // Waiting, Compiling, Testing, Done
    ErrorCode ErrorCode,  // None, CompileError, RuntimeError, etc.
    string Message,
    int CurrentTest,
    int AmountOfTests
);

Быстрый старт

Локальная разработка (Docker Compose)

# Сборка и запуск всех сервисов
docker-compose up --build

# Gateway доступен на http://localhost:8080
# Worker доступен на http://localhost:8081

Разработка без Docker

# Сборка всего решения
dotnet build

# Запуск Gateway
cd src/LiquidCode.Tester.Gateway
dotnet run

# Запуск Worker (в другом терминале)
cd src/LiquidCode.Tester.Worker
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. Отправка запроса

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

Gateway

POST /api/tester/submit

Отправка решения с удаленным пакетом тестов:

{
  "id": 123,
  "missionId": 456,
  "language": "C++",
  "languageVersion": "17",
  "sourceCode": "#include <iostream>\nint main() { ... }",
  "packageUrl": "https://example.com/package.zip",
  "callbackUrl": "https://example.com/api/submit/callback"
}

POST /api/tester/submit-local

Отправка решения с локальным пакетом тестов (для отладки):

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

  • Проверка состояния Gateway

Worker

POST /api/test

  • Принимает multipart/form-data с метаданными и ZIP файлом пакета

GET /api/test/health

  • Проверка состояния Worker

Процесс тестирования

  1. Приём запроса: Gateway получает запрос с кодом и URL пакета
  2. Скачивание пакета: Gateway скачивает Polygon пакет
  3. Маршрутизация: Gateway отправляет запрос в Worker для нужного языка
  4. Парсинг: Worker распаковывает и парсит пакет (тесты, лимиты)
  5. Компиляция: Worker выбирает нужный компилятор на основе языка и компилирует код
  6. Тестирование: Worker последовательно запускает все тесты
  7. Callback: Worker отправляет статусы на callback URL на каждом этапе
  8. Cleanup: Worker удаляет временные файлы

Изоляция и безопасность

Текущая реализация

  • Процессы запускаются с ограничением по времени
  • Контроль памяти через PeakWorkingSet64
  • Процессы убиваются при превышении лимитов

Production рекомендации

  • Linux namespaces (PID, network, mount)
  • cgroups для контроля CPU и памяти
  • seccomp для ограничения системных вызовов
  • AppArmor/SELinux профили
  • chroot окружение
  • Отключение доступа к сети для тестируемого кода

Конфигурация

Gateway (appsettings.json)

{
  "PackageDownloadDirectory": "/tmp/packages",
  "Workers": {
    "Cpp": "http://localhost:8081",
    "Java": "http://localhost:8081",
    "Kotlin": "http://localhost:8081",
    "CSharp": "http://localhost:8081",
    "Python": "http://localhost:8081"
  }
}

Worker (appsettings.json)

{
  "Cpp": {
    "Compiler": "g++",
    "CompilerFlags": "-O2 -std=c++17 -Wall",
    "Versions": {
      "14": {
        "Compiler": "g++",
        "CompilerFlags": "-O2 -std=c++14 -Wall"
      },
      "17": {
        "Compiler": "g++",
        "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): путь к интерпретатору

Пример использования:

# Использование 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++' \
  ...

Структура проекта

LiquidCode.Tester/
├── src/
│   ├── LiquidCode.Tester.Common/        # Общие модели
│   │   └── Models/
│   ├── LiquidCode.Tester.Gateway/       # API Gateway
│   │   ├── Controllers/
│   │   ├── Services/
│   │   └── Dockerfile
│   └── LiquidCode.Tester.Worker/        # Универсальный Worker (все языки)
│       ├── Controllers/
│       ├── Services/
│       │   ├── Compilation/             # Компиляторы
│       │   ├── Execution/               # Исполнители
│       │   └── Factories/               # Фабрики
│       └── Dockerfile
├── compose.yaml                          # Docker Compose
└── README.md

Требования

  • .NET 9.0 SDK
  • Docker & Docker Compose (для контейнеризации)
  • Компиляторы языков программирования (устанавливаются автоматически в Docker):
    • g++ (C++)
    • OpenJDK 17 (Java)
    • Kotlin compiler (Kotlin)
    • Mono (C#)
    • Python 3

Расширение на другие языки

Для добавления поддержки нового языка:

  1. Создайте новую реализацию ICompilationService для языка (например, GoCompilationService.cs)
  2. Создайте новую реализацию IExecutionService для языка (например, GoExecutionService.cs)
  3. Зарегистрируйте новые сервисы в Program.cs:
    builder.Services.AddSingleton<GoCompilationService>();
    builder.Services.AddSingleton<GoExecutionService>();
    
  4. Обновите фабрики (CompilationServiceFactory и ExecutionServiceFactory), добавив поддержку нового языка
  5. Добавьте конфигурацию языка в appsettings.json Worker
  6. Обновите Dockerfile Worker, добавив установку компилятора/runtime
  7. Добавьте конфигурацию воркера для нового языка в appsettings.json Gateway
  8. Обновите WorkerClientService Gateway, добавив маппинг языка на URL воркера

Разработка

Структура кода

  • Controllers: REST API endpoints
  • Services: Бизнес-логика (компиляция, тестирование, callback)
  • Models: Модели данных

Логирование

Используется встроенный ILogger ASP.NET Core.

Тестирование

dotnet test

Лицензия

[Укажите лицензию]

Авторы

LiquidCode Team

Description
Модуль тестирования.
Readme 8.7 MiB
Languages
C# 46.4%
C++ 38%
TeX 6.4%
Shell 3%
Batchfile 2.4%
Other 3.8%