update polygon package parsing & testing
This commit is contained in:
579
POLYGON_PACKAGE_STRUCTURE.md
Normal file
579
POLYGON_PACKAGE_STRUCTURE.md
Normal file
@@ -0,0 +1,579 @@
|
||||
# Структура пакета Polygon
|
||||
|
||||
## 📦 Типичная структура Polygon пакета
|
||||
|
||||
```
|
||||
problem-name.zip
|
||||
│
|
||||
├── problem.xml ← Главный дескриптор задачи
|
||||
│
|
||||
├── tests/ ← Тесты для проверки решений
|
||||
│ ├── 01 ← Входные данные (без расширения!)
|
||||
│ ├── 01.a ← Ответы (могут отсутствовать)
|
||||
│ ├── 02
|
||||
│ ├── 02.a
|
||||
│ └── ...
|
||||
│
|
||||
├── files/ ← Вспомогательные файлы
|
||||
│ ├── testlib.h ← Библиотека для checker/validator/generator
|
||||
│ ├── olymp.sty ← LaTeX стиль для statements
|
||||
│ ├── problem.tex ← Шаблон условия
|
||||
│ ├── statements.ftl ← FreeMarker шаблон
|
||||
│ │
|
||||
│ ├── tests/ ← Тесты для checker/validator
|
||||
│ │ ├── checker-tests/
|
||||
│ │ │ ├── 01
|
||||
│ │ │ ├── 01.a
|
||||
│ │ │ └── 01.o ← Ожидаемый output
|
||||
│ │ │
|
||||
│ │ └── validator-tests/
|
||||
│ │ ├── 01
|
||||
│ │ ├── 02
|
||||
│ │ └── ...
|
||||
│ │
|
||||
│ ├── g.cpp ← Generator (генератор тестов)
|
||||
│ ├── g.exe
|
||||
│ ├── v.cpp ← Validator (валидатор входных данных)
|
||||
│ ├── v.exe
|
||||
│ ├── check.cpp ← Checker (проверка ответа)
|
||||
│ ├── check.exe
|
||||
│ ├── interactor.cpp ← Interactor (для интерактивных задач)
|
||||
│ ├── interactor.exe
|
||||
│ │
|
||||
│ └── [resource files] ← Дополнительные ресурсы для решений
|
||||
│ ├── aplusb.h ← Header для grader-задач
|
||||
│ ├── grader.cpp ← Grader для компиляции с решением
|
||||
│ └── main.py ← Python wrapper для grader
|
||||
│
|
||||
├── solutions/ ← Эталонные решения
|
||||
│ ├── sol.cpp ← Главное (main) решение
|
||||
│ ├── sol.exe
|
||||
│ ├── sol.py
|
||||
│ ├── sol.java
|
||||
│ │
|
||||
│ ├── sol-accepted-1.cpp ← Дополнительные AC решения
|
||||
│ ├── sol-wa.cpp ← Wrong Answer решения (для теста)
|
||||
│ ├── sol-tl.cpp ← Time Limit решения
|
||||
│ ├── sol-ml.cpp ← Memory Limit решения
|
||||
│ └── ...
|
||||
│
|
||||
├── statements/ ← Условия задачи
|
||||
│ ├── english/
|
||||
│ │ ├── problem.tex ← Исходник условия (LaTeX)
|
||||
│ │ ├── problem-properties.json
|
||||
│ │ ├── tutorial.tex ← Разбор задачи
|
||||
│ │ ├── example.01 ← Примеры из условия (input)
|
||||
│ │ └── example.01.a ← Примеры из условия (output)
|
||||
│ │
|
||||
│ ├── russian/
|
||||
│ │ └── ...
|
||||
│ │
|
||||
│ ├── .html/ ← Сгенерированные HTML
|
||||
│ │ ├── english/
|
||||
│ │ │ ├── problem.html
|
||||
│ │ │ ├── tutorial.html
|
||||
│ │ │ └── problem-statement.css
|
||||
│ │ └── russian/
|
||||
│ │
|
||||
│ └── .pdf/ ← Сгенерированные PDF
|
||||
│ ├── english/
|
||||
│ │ ├── problem.pdf
|
||||
│ │ └── tutorial.pdf
|
||||
│ └── russian/
|
||||
│
|
||||
├── statement-sections/ ← Секции условия (модульно)
|
||||
│ ├── english/
|
||||
│ │ ├── legend.tex
|
||||
│ │ ├── input.tex
|
||||
│ │ ├── output.tex
|
||||
│ │ ├── notes.tex
|
||||
│ │ ├── scoring.tex
|
||||
│ │ └── examples/
|
||||
│ └── russian/
|
||||
│
|
||||
├── materials/ ← Материалы для участников
|
||||
│ ├── grader-cpp.zip ← Grader для C++
|
||||
│ ├── grader-py.zip ← Grader для Python
|
||||
│ └── ...
|
||||
│
|
||||
├── scripts/ ← Скрипты для работы с пакетом
|
||||
│ ├── gen-answer.sh
|
||||
│ ├── gen-input-via-files.sh
|
||||
│ ├── run-validator-tests.sh
|
||||
│ └── ...
|
||||
│
|
||||
├── check.cpp ← Checker в корне (копия)
|
||||
├── check.exe
|
||||
├── doall.sh ← Скрипт сборки всего
|
||||
├── doall.bat
|
||||
├── wipe.sh ← Скрипт очистки
|
||||
├── wipe.bat
|
||||
└── tags ← Теги задачи (metadata)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 Основные файлы и их назначение
|
||||
|
||||
### 1. **problem.xml** (обязательный)
|
||||
|
||||
Центральный дескриптор задачи:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<problem revision="17" short-name="example-problem" url="...">
|
||||
<names>
|
||||
<name language="english" value="Problem Title"/>
|
||||
</names>
|
||||
|
||||
<judging>
|
||||
<testset name="tests">
|
||||
<time-limit>2000</time-limit> <!-- мс -->
|
||||
<memory-limit>268435456</memory-limit> <!-- байты = 256 MB -->
|
||||
<test-count>51</test-count>
|
||||
<input-path-pattern>tests/%02d</input-path-pattern>
|
||||
<answer-path-pattern>tests/%02d.a</answer-path-pattern>
|
||||
|
||||
<tests>
|
||||
<test method="manual" sample="true"/> <!-- ручной, в примерах -->
|
||||
<test cmd="gen 100" method="generated"/> <!-- сгенерированный -->
|
||||
<test cmd="gen 1000" method="generated"/>
|
||||
...
|
||||
</tests>
|
||||
</testset>
|
||||
</judging>
|
||||
|
||||
<assets>
|
||||
<checker name="std::ncmp.cpp" type="testlib">
|
||||
<source path="files/check.cpp" type="cpp.g++17"/>
|
||||
<binary path="check.exe" type="exe.win32"/>
|
||||
</checker>
|
||||
|
||||
<validators>
|
||||
<validator>
|
||||
<source path="files/v.cpp" type="cpp.g++17"/>
|
||||
<binary path="files/v.exe" type="exe.win32"/>
|
||||
</validator>
|
||||
</validators>
|
||||
|
||||
<solutions>
|
||||
<solution tag="main">
|
||||
<source path="solutions/sol.cpp" type="cpp.g++17"/>
|
||||
<binary path="solutions/sol.exe" type="exe.win32"/>
|
||||
</solution>
|
||||
<solution tag="accepted">
|
||||
<source path="solutions/sol.py" type="python.3"/>
|
||||
</solution>
|
||||
<solution tag="wrong-answer">
|
||||
<source path="solutions/sol-wa.cpp" type="cpp.g++17"/>
|
||||
</solution>
|
||||
</solutions>
|
||||
</assets>
|
||||
</problem>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. **testlib.h** (стандартная библиотека)
|
||||
|
||||
Библиотека от MikeMirzayanov для написания:
|
||||
- **Checkers** - проверка правильности ответа
|
||||
- **Validators** - проверка корректности входных данных
|
||||
- **Generators** - генерация тестов
|
||||
- **Interactors** - интерактивное взаимодействие
|
||||
|
||||
**Основные функции:**
|
||||
|
||||
```cpp
|
||||
#include "testlib.h"
|
||||
|
||||
// Checker
|
||||
int main(int argc, char* argv[]) {
|
||||
registerTestlibCmd(argc, argv);
|
||||
|
||||
int jans = ans.readInt(); // Правильный ответ
|
||||
int pans = ouf.readInt(); // Ответ участника
|
||||
|
||||
if (jans == pans)
|
||||
quitf(_ok, "Correct");
|
||||
else
|
||||
quitf(_wa, "Wrong answer: %d instead of %d", pans, jans);
|
||||
}
|
||||
|
||||
// Validator
|
||||
int main(int argc, char* argv[]) {
|
||||
registerValidation(argc, argv);
|
||||
|
||||
int n = inf.readInt(1, 100000, "n");
|
||||
inf.readEoln();
|
||||
inf.readEof();
|
||||
}
|
||||
|
||||
// Generator
|
||||
int main(int argc, char* argv[]) {
|
||||
registerGen(argc, argv, 1);
|
||||
|
||||
int n = opt<int>("n");
|
||||
println(n);
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
println(rnd.next(1, 1000000));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. **Тесты (tests/)**
|
||||
|
||||
**Формат:**
|
||||
```
|
||||
tests/01 ← Входные данные (plain text, без расширения)
|
||||
tests/01.a ← Ответ (answer file)
|
||||
```
|
||||
|
||||
**Пример:**
|
||||
```
|
||||
# tests/01 (input)
|
||||
2 3
|
||||
|
||||
# tests/01.a (answer)
|
||||
5
|
||||
```
|
||||
|
||||
**Метаданные из problem.xml:**
|
||||
```xml
|
||||
<tests>
|
||||
<test method="manual" sample="true"/> <!-- tests/01 - вручную, в примерах -->
|
||||
<test cmd="gen 10 5" method="generated"/> <!-- tests/02 - сгенерирован -->
|
||||
<test cmd="gen 100 50" method="generated"/> <!-- tests/03 -->
|
||||
</tests>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. **Generator (g.cpp)**
|
||||
|
||||
Программа для генерации тестов:
|
||||
|
||||
```cpp
|
||||
#include "testlib.h"
|
||||
#include <iostream>
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
registerGen(argc, argv, 1);
|
||||
|
||||
int n = opt<int>(1); // Первый аргумент
|
||||
int maxVal = opt<int>(2); // Второй аргумент
|
||||
|
||||
std::cout << n << std::endl;
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
std::cout << rnd.next(1, maxVal);
|
||||
if (i + 1 < n) std::cout << " ";
|
||||
}
|
||||
std::cout << std::endl;
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**Использование:**
|
||||
```bash
|
||||
# В problem.xml указано:
|
||||
<test cmd="gen 1000 10000" method="generated"/>
|
||||
|
||||
# Polygon запускает:
|
||||
g.exe 1000 10000 > tests/05
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. **Checker (check.cpp)**
|
||||
|
||||
Программа для проверки корректности ответа.
|
||||
|
||||
**Типы checkers:**
|
||||
|
||||
#### **A. Стандартные (встроенные в testlib.h):**
|
||||
```cpp
|
||||
std::ncmp.cpp // Сравнение одного целого числа
|
||||
std::fcmp.cpp // Сравнение одного float с точностью
|
||||
std::wcmp.cpp // Сравнение по словам (tokens)
|
||||
std::lcmp.cpp // Построчное сравнение
|
||||
std::nyesno.cpp // Проверка YES/NO
|
||||
```
|
||||
|
||||
#### **B. Custom checker:**
|
||||
```cpp
|
||||
#include "testlib.h"
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
registerTestlibCmd(argc, argv);
|
||||
|
||||
// inf - входной файл (input)
|
||||
// ouf - output участника (output user file)
|
||||
// ans - правильный ответ (answer)
|
||||
|
||||
int n = inf.readInt();
|
||||
|
||||
std::vector<int> jans(n);
|
||||
for (int i = 0; i < n; i++)
|
||||
jans[i] = ans.readInt();
|
||||
|
||||
std::vector<int> pans(n);
|
||||
for (int i = 0; i < n; i++)
|
||||
pans[i] = ouf.readInt();
|
||||
|
||||
// Проверка: порядок не важен (множества равны)
|
||||
std::sort(jans.begin(), jans.end());
|
||||
std::sort(pans.begin(), pans.end());
|
||||
|
||||
if (jans == pans)
|
||||
quitf(_ok, "Correct");
|
||||
else
|
||||
quitf(_wa, "Wrong answer");
|
||||
}
|
||||
```
|
||||
|
||||
**Exit codes:**
|
||||
- `0` - OK (правильный ответ) ✅
|
||||
- `1` - WA (неправильный ответ) ❌
|
||||
- `2` - PE (presentation error)
|
||||
- `3` - FAIL (ошибка в самом чекере)
|
||||
- `7` - Partial (частичный балл, для IOI-style)
|
||||
|
||||
---
|
||||
|
||||
### 6. **Validator (v.cpp)**
|
||||
|
||||
Проверяет корректность входных данных:
|
||||
|
||||
```cpp
|
||||
#include "testlib.h"
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
registerValidation(argc, argv);
|
||||
|
||||
// Проверка формата входных данных
|
||||
int n = inf.readInt(1, 100000, "n"); // 1 ≤ n ≤ 100000
|
||||
inf.readEoln(); // Конец строки
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
inf.readInt(1, 1000000000, "a[i]");
|
||||
if (i + 1 < n) inf.readSpace();
|
||||
else inf.readEoln();
|
||||
}
|
||||
|
||||
inf.readEof(); // Конец файла
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**Назначение:**
|
||||
- Проверка ограничений (1 ≤ n ≤ 10⁶)
|
||||
- Проверка формата (пробелы, переводы строк)
|
||||
- Валидация структуры (дерево, граф и т.д.)
|
||||
|
||||
---
|
||||
|
||||
### 7. **Interactor (для интерактивных задач)**
|
||||
|
||||
Посредник между решением и тестирующей системой:
|
||||
|
||||
```cpp
|
||||
#include "testlib.h"
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
registerInteraction(argc, argv);
|
||||
|
||||
int n = inf.readInt(); // Загаданное число
|
||||
|
||||
int queries = 0;
|
||||
while (queries < 20) {
|
||||
int guess = ouf.readInt(1, 1000000); // Запрос участника
|
||||
queries++;
|
||||
|
||||
if (guess == n) {
|
||||
tout << "YES" << endl;
|
||||
quitf(_ok, "Found in %d queries", queries);
|
||||
} else if (guess < n) {
|
||||
tout << ">" << endl; // Больше
|
||||
} else {
|
||||
tout << "<" << endl; // Меньше
|
||||
}
|
||||
}
|
||||
|
||||
quitf(_wa, "Too many queries");
|
||||
}
|
||||
```
|
||||
|
||||
**Streams в interactor:**
|
||||
- `inf` - входной файл (input)
|
||||
- `ouf` - output участника (чтение запросов)
|
||||
- `tout` - передача данных участнику (ответы на запросы)
|
||||
- `ans` - правильный ответ (не используется в интерактивных)
|
||||
|
||||
---
|
||||
|
||||
### 8. **Solutions (эталонные решения)**
|
||||
|
||||
**Типы решений:**
|
||||
|
||||
```xml
|
||||
<solution tag="main"> <!-- Главное решение (генерирует .a файлы) -->
|
||||
<solution tag="accepted"> <!-- Дополнительные AC решения -->
|
||||
<solution tag="wrong-answer"> <!-- WA (для теста checker'а) -->
|
||||
<solution tag="time-limit-exceeded"> <!-- TL -->
|
||||
<solution tag="memory-limit-exceeded"> <!-- ML -->
|
||||
<solution tag="rejected"> <!-- Другие RE/PE -->
|
||||
```
|
||||
|
||||
**Назначение:**
|
||||
- `main` - используется для генерации answer files
|
||||
- `accepted` - проверка, что задача решаема разными способами
|
||||
- `wrong-answer` - тестирование checker'а
|
||||
- `time-limit-exceeded` - проверка TL
|
||||
|
||||
---
|
||||
|
||||
### 9. **Grader-задачи (специальный тип)**
|
||||
|
||||
Участник пишет функцию, а не всю программу.
|
||||
|
||||
**Структура:**
|
||||
```
|
||||
files/
|
||||
├── aplusb.h ← Header с сигнатурой функции
|
||||
├── grader.cpp ← Main + вызов функции участника
|
||||
└── main.py ← Python wrapper
|
||||
|
||||
solutions/
|
||||
└── sol.cpp ← Реализация функции (не main!)
|
||||
```
|
||||
|
||||
**Пример:**
|
||||
```cpp
|
||||
// aplusb.h
|
||||
int sum(int a, int b);
|
||||
|
||||
// grader.cpp
|
||||
#include "aplusb.h"
|
||||
#include <iostream>
|
||||
|
||||
int main() {
|
||||
int a, b;
|
||||
std::cin >> a >> b;
|
||||
std::cout << sum(a, b) << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// sol.cpp (решение участника)
|
||||
#include "aplusb.h"
|
||||
|
||||
int sum(int a, int b) {
|
||||
return a + b;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Статистика по примерам пакетов
|
||||
|
||||
### **a-plus-b-graders-7.zip:**
|
||||
```
|
||||
Размер: 2.4 MB
|
||||
Файлов: ~50
|
||||
Структура:
|
||||
✅ problem.xml
|
||||
✅ tests/ (8 тестов: 01-08, без .a файлов)
|
||||
✅ files/testlib.h
|
||||
✅ files/aplusb.h (grader header)
|
||||
✅ files/grader.cpp
|
||||
✅ files/g.cpp, g.exe (generator)
|
||||
✅ files/v.cpp, v.exe (validator)
|
||||
✅ check.cpp, check.exe (ncmp - числовой checker)
|
||||
✅ solutions/sol.cpp (main)
|
||||
✅ solutions/sol.py (accepted)
|
||||
✅ statements/english/ (условие)
|
||||
```
|
||||
|
||||
### **example-interactive-binary-search-26.zip:**
|
||||
```
|
||||
Размер: 10.7 MB
|
||||
Файлов: ~100+
|
||||
Структура:
|
||||
✅ problem.xml
|
||||
✅ tests/ (21 тест: 01-21, без .a файлов)
|
||||
✅ files/testlib.h
|
||||
✅ files/interactor.cpp, interactor.exe ← ИНТЕРАКТИВНАЯ!
|
||||
✅ files/gen.cpp, gen.exe
|
||||
✅ files/val.cpp, val.exe
|
||||
✅ check.cpp, check.exe
|
||||
✅ solutions/ (16 решений: main, ac, wa, tl, ml, pe, ...)
|
||||
✅ statements/english/ + russian/
|
||||
```
|
||||
|
||||
### **exam-queue-17.zip:**
|
||||
```
|
||||
Размер: 6.6 MB
|
||||
Файлов: ~80
|
||||
Структура:
|
||||
✅ problem.xml
|
||||
✅ tests/ (51 тест: 01-51, без .a файлов)
|
||||
✅ files/testlib.h
|
||||
✅ files/gen.cpp, gen.exe
|
||||
✅ files/val.cpp, val.exe
|
||||
✅ check.cpp, check.exe
|
||||
✅ solutions/ (множество решений)
|
||||
✅ statements/russian/ (только русское условие)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Ключевые выводы
|
||||
|
||||
### **Обязательные компоненты:**
|
||||
1. ✅ `problem.xml` - дескриптор
|
||||
2. ✅ `tests/` - тесты
|
||||
3. ✅ `files/testlib.h` - библиотека
|
||||
4. ✅ `solutions/` - хотя бы одно main решение
|
||||
|
||||
### **Опциональные (но часто присутствуют):**
|
||||
- `check.cpp` - custom checker (иначе используется wcmp)
|
||||
- `v.cpp` - validator
|
||||
- `g.cpp` - generator
|
||||
- `interactor.cpp` - для интерактивных задач
|
||||
- `statements/` - условия на разных языках
|
||||
- `*.a` файлы - ответы (генерируются из main solution)
|
||||
|
||||
### **Особенности формата:**
|
||||
- Входные файлы **БЕЗ расширения** (01, 02, не 01.in!)
|
||||
- Ответы с расширением `.a` (01.a, 02.a)
|
||||
- testlib.h - единая библиотека для всего
|
||||
- problem.xml - полное описание всего пакета
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Полезные ссылки
|
||||
|
||||
- **Polygon:** https://polygon.codeforces.com/
|
||||
- **testlib.h GitHub:** https://github.com/MikeMirzayanov/testlib
|
||||
- **Документация testlib:** https://codeforces.com/testlib
|
||||
- **Tutorial по Polygon:** https://codeforces.com/blog/entry/101072
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Поддержка в LiquidCode.Tester
|
||||
|
||||
Наша система **полностью поддерживает**:
|
||||
- ✅ Парсинг problem.xml
|
||||
- ✅ Тесты в формате tests/01, tests/01.a
|
||||
- ✅ Автоматическую генерацию answer файлов из main solution
|
||||
- ✅ Компиляцию и запуск custom checkers (testlib-based)
|
||||
- ✅ Определение лимитов времени/памяти из problem.xml
|
||||
- ✅ Поддержку всех основных языков (C++, Java, Python, C#, Kotlin)
|
||||
|
||||
**В разработке:**
|
||||
- ⏳ Поддержка interactor для интерактивных задач
|
||||
- ⏳ Поддержка grader-задач
|
||||
- ⏳ Запуск validator для проверки входных данных
|
||||
@@ -10,6 +10,9 @@ builder.Services.AddOpenApi();
|
||||
builder.Services.AddHttpClient();
|
||||
|
||||
// Register application services
|
||||
builder.Services.AddSingleton<PolygonProblemXmlParser>();
|
||||
builder.Services.AddSingleton<AnswerGenerationService>();
|
||||
builder.Services.AddSingleton<CheckerService>();
|
||||
builder.Services.AddSingleton<IPackageParserService, PackageParserService>();
|
||||
builder.Services.AddSingleton<IOutputCheckerService, OutputCheckerService>();
|
||||
builder.Services.AddSingleton<ICallbackService, CallbackService>();
|
||||
|
||||
181
src/LiquidCode.Tester.Worker/Services/AnswerGenerationService.cs
Normal file
181
src/LiquidCode.Tester.Worker/Services/AnswerGenerationService.cs
Normal file
@@ -0,0 +1,181 @@
|
||||
namespace LiquidCode.Tester.Worker.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for generating answer files by running the main solution from Polygon package
|
||||
/// </summary>
|
||||
public class AnswerGenerationService
|
||||
{
|
||||
private readonly ICompilationServiceFactory _compilationFactory;
|
||||
private readonly IExecutionServiceFactory _executionFactory;
|
||||
private readonly ILogger<AnswerGenerationService> _logger;
|
||||
|
||||
public AnswerGenerationService(
|
||||
ICompilationServiceFactory compilationFactory,
|
||||
IExecutionServiceFactory executionFactory,
|
||||
ILogger<AnswerGenerationService> logger)
|
||||
{
|
||||
_compilationFactory = compilationFactory;
|
||||
_executionFactory = executionFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<bool> GenerateAnswersAsync(
|
||||
PolygonProblemDescriptor descriptor,
|
||||
string workingDirectory,
|
||||
List<string> inputFilePaths,
|
||||
List<string> answerFilePaths)
|
||||
{
|
||||
if (string.IsNullOrEmpty(descriptor.MainSolutionPath))
|
||||
{
|
||||
_logger.LogWarning("No main solution specified, cannot generate answers");
|
||||
return false;
|
||||
}
|
||||
|
||||
var solutionPath = Path.Combine(workingDirectory, descriptor.MainSolutionPath);
|
||||
if (!File.Exists(solutionPath))
|
||||
{
|
||||
_logger.LogWarning("Main solution file not found: {Path}", solutionPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine language and version from solution type
|
||||
var (language, version) = ParseSolutionType(descriptor.MainSolutionType ?? "");
|
||||
if (language == null)
|
||||
{
|
||||
_logger.LogWarning("Unsupported solution type: {Type}", descriptor.MainSolutionType);
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Generating answers using {Language} {Version} solution: {Path}",
|
||||
language, version, descriptor.MainSolutionPath);
|
||||
|
||||
try
|
||||
{
|
||||
// Read solution source code
|
||||
var sourceCode = await File.ReadAllTextAsync(solutionPath);
|
||||
|
||||
// Get compilation service
|
||||
var compilationService = _compilationFactory.GetCompilationService(language);
|
||||
var executionService = _executionFactory.GetExecutionService(language);
|
||||
|
||||
// Compile solution
|
||||
_logger.LogInformation("Compiling main solution...");
|
||||
var compilationResult = await compilationService.CompileAsync(
|
||||
sourceCode,
|
||||
Path.GetDirectoryName(solutionPath)!,
|
||||
version);
|
||||
|
||||
if (!compilationResult.Success)
|
||||
{
|
||||
_logger.LogError("Failed to compile main solution: {Error}", compilationResult.CompilerOutput);
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Main solution compiled successfully");
|
||||
|
||||
// Generate answers for each test
|
||||
int generatedCount = 0;
|
||||
for (int i = 0; i < inputFilePaths.Count; i++)
|
||||
{
|
||||
var inputPath = inputFilePaths[i];
|
||||
var answerPath = answerFilePaths[i];
|
||||
|
||||
if (!File.Exists(inputPath))
|
||||
{
|
||||
_logger.LogWarning("Input file not found: {Path}", inputPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Generating answer {Index}/{Total}: {AnswerPath}",
|
||||
i + 1, inputFilePaths.Count, answerPath);
|
||||
|
||||
// Execute solution with input
|
||||
var executionResult = await executionService.ExecuteAsync(
|
||||
compilationResult.ExecutablePath!,
|
||||
inputPath,
|
||||
descriptor.TimeLimitMs * 2, // Give extra time for answer generation
|
||||
descriptor.MemoryLimitMb * 2);
|
||||
|
||||
if (!executionResult.Success || executionResult.RuntimeError)
|
||||
{
|
||||
_logger.LogWarning("Failed to generate answer for {InputPath}: {Error}",
|
||||
inputPath, executionResult.ErrorMessage);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Save output as answer file
|
||||
var answerDir = Path.GetDirectoryName(answerPath);
|
||||
if (!string.IsNullOrEmpty(answerDir) && !Directory.Exists(answerDir))
|
||||
{
|
||||
Directory.CreateDirectory(answerDir);
|
||||
}
|
||||
|
||||
await File.WriteAllTextAsync(answerPath, executionResult.Output);
|
||||
generatedCount++;
|
||||
|
||||
_logger.LogDebug("Generated answer {Index}/{Total}", i + 1, inputFilePaths.Count);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Generated {Count} answer files out of {Total} tests",
|
||||
generatedCount, inputFilePaths.Count);
|
||||
|
||||
return generatedCount > 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error generating answers");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private (string? language, string version) ParseSolutionType(string solutionType)
|
||||
{
|
||||
// Polygon solution types: python.2, python.3, cpp.g++17, cpp.g++20, java7, java8, etc.
|
||||
if (string.IsNullOrEmpty(solutionType))
|
||||
{
|
||||
return (null, "");
|
||||
}
|
||||
|
||||
if (solutionType.StartsWith("python."))
|
||||
{
|
||||
var parts = solutionType.Split('.');
|
||||
var version = parts.Length > 1 ? parts[1] : "3";
|
||||
return ("python", $"3.{version}"); // Map python.3 -> 3.3, python.2 -> 3.2 (approx)
|
||||
}
|
||||
|
||||
if (solutionType.StartsWith("cpp."))
|
||||
{
|
||||
// cpp.g++17, cpp.g++20, cpp.g++14
|
||||
if (solutionType.Contains("++20"))
|
||||
return ("cpp", "20");
|
||||
if (solutionType.Contains("++17"))
|
||||
return ("cpp", "17");
|
||||
if (solutionType.Contains("++14"))
|
||||
return ("cpp", "14");
|
||||
return ("cpp", "17"); // Default to C++17
|
||||
}
|
||||
|
||||
if (solutionType.StartsWith("java"))
|
||||
{
|
||||
// java7, java8, java11
|
||||
if (solutionType.Contains("11"))
|
||||
return ("java", "11");
|
||||
if (solutionType.Contains("8"))
|
||||
return ("java", "8");
|
||||
return ("java", "11"); // Default to Java 11
|
||||
}
|
||||
|
||||
if (solutionType.StartsWith("csharp"))
|
||||
{
|
||||
return ("csharp", "9"); // Default to C# 9
|
||||
}
|
||||
|
||||
if (solutionType.StartsWith("kotlin"))
|
||||
{
|
||||
return ("kotlin", "1.9"); // Default to Kotlin 1.9
|
||||
}
|
||||
|
||||
_logger.LogWarning("Unknown solution type: {Type}", solutionType);
|
||||
return (null, "");
|
||||
}
|
||||
}
|
||||
165
src/LiquidCode.Tester.Worker/Services/CheckerService.cs
Normal file
165
src/LiquidCode.Tester.Worker/Services/CheckerService.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace LiquidCode.Tester.Worker.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for running custom checkers (testlib-based)
|
||||
/// </summary>
|
||||
public class CheckerService
|
||||
{
|
||||
private readonly ILogger<CheckerService> _logger;
|
||||
|
||||
public CheckerService(ILogger<CheckerService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check user output using custom checker
|
||||
/// </summary>
|
||||
/// <param name="checkerPath">Path to checker executable</param>
|
||||
/// <param name="inputPath">Path to input file</param>
|
||||
/// <param name="userOutput">User program output</param>
|
||||
/// <param name="answerPath">Path to answer file</param>
|
||||
/// <returns>Checker result</returns>
|
||||
public async Task<CheckerResult> CheckAsync(
|
||||
string checkerPath,
|
||||
string inputPath,
|
||||
string userOutput,
|
||||
string answerPath)
|
||||
{
|
||||
if (!File.Exists(checkerPath))
|
||||
{
|
||||
_logger.LogError("Checker not found: {CheckerPath}", checkerPath);
|
||||
return new CheckerResult
|
||||
{
|
||||
Accepted = false,
|
||||
ExitCode = -1,
|
||||
Message = "Checker executable not found"
|
||||
};
|
||||
}
|
||||
|
||||
// Save user output to temporary file
|
||||
var tempOutputPath = Path.Combine(Path.GetTempPath(), $"user_output_{Guid.NewGuid()}.txt");
|
||||
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(tempOutputPath, userOutput);
|
||||
|
||||
_logger.LogDebug("Running checker: {Checker} {Input} {Output} {Answer}",
|
||||
checkerPath, inputPath, tempOutputPath, answerPath);
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = checkerPath,
|
||||
Arguments = $"\"{inputPath}\" \"{tempOutputPath}\" \"{answerPath}\"",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
|
||||
var stdout = await process.StandardOutput.ReadToEndAsync();
|
||||
var stderr = await process.StandardError.ReadToEndAsync();
|
||||
|
||||
// Wait with timeout (checkers should be fast)
|
||||
var completed = await Task.Run(() => process.WaitForExit(5000));
|
||||
|
||||
if (!completed)
|
||||
{
|
||||
_logger.LogWarning("Checker timeout, killing process");
|
||||
try
|
||||
{
|
||||
process.Kill(entireProcessTree: true);
|
||||
}
|
||||
catch { }
|
||||
|
||||
return new CheckerResult
|
||||
{
|
||||
Accepted = false,
|
||||
ExitCode = -1,
|
||||
Message = "Checker timeout"
|
||||
};
|
||||
}
|
||||
|
||||
var exitCode = process.ExitCode;
|
||||
|
||||
_logger.LogDebug("Checker exit code: {ExitCode}, stderr: {Stderr}", exitCode, stderr);
|
||||
|
||||
return new CheckerResult
|
||||
{
|
||||
Accepted = exitCode == 0,
|
||||
ExitCode = exitCode,
|
||||
Message = string.IsNullOrWhiteSpace(stderr) ? stdout : stderr,
|
||||
Verdict = GetVerdictFromExitCode(exitCode)
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error running checker");
|
||||
return new CheckerResult
|
||||
{
|
||||
Accepted = false,
|
||||
ExitCode = -1,
|
||||
Message = $"Checker error: {ex.Message}"
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup temporary file
|
||||
try
|
||||
{
|
||||
if (File.Exists(tempOutputPath))
|
||||
{
|
||||
File.Delete(tempOutputPath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to delete temporary output file: {Path}", tempOutputPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private CheckerVerdict GetVerdictFromExitCode(int exitCode)
|
||||
{
|
||||
return exitCode switch
|
||||
{
|
||||
0 => CheckerVerdict.OK,
|
||||
1 => CheckerVerdict.WrongAnswer,
|
||||
2 => CheckerVerdict.PresentationError,
|
||||
3 => CheckerVerdict.CheckerFailed,
|
||||
7 => CheckerVerdict.PartialScore,
|
||||
_ => CheckerVerdict.Unknown
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of custom checker execution
|
||||
/// </summary>
|
||||
public class CheckerResult
|
||||
{
|
||||
public bool Accepted { get; set; }
|
||||
public int ExitCode { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public CheckerVerdict Verdict { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checker verdict codes (testlib standard)
|
||||
/// </summary>
|
||||
public enum CheckerVerdict
|
||||
{
|
||||
OK = 0, // Accepted
|
||||
WrongAnswer = 1, // Wrong Answer
|
||||
PresentationError = 2, // Presentation Error (format issue)
|
||||
CheckerFailed = 3, // Internal checker error
|
||||
PartialScore = 7, // Partial score (for IOI-style)
|
||||
Unknown = -1 // Unknown exit code
|
||||
}
|
||||
@@ -9,4 +9,14 @@ public interface IOutputCheckerService
|
||||
/// <param name="expectedOutputPath">Path to expected output file</param>
|
||||
/// <returns>True if outputs match</returns>
|
||||
Task<bool> CheckOutputAsync(string actualOutput, string expectedOutputPath);
|
||||
|
||||
/// <summary>
|
||||
/// Checks output using custom checker if available, falls back to standard checking
|
||||
/// </summary>
|
||||
/// <param name="actualOutput">Output from user's solution</param>
|
||||
/// <param name="inputFilePath">Path to input file</param>
|
||||
/// <param name="expectedOutputPath">Path to expected output file</param>
|
||||
/// <param name="checkerPath">Path to custom checker executable (optional)</param>
|
||||
/// <returns>True if output is accepted</returns>
|
||||
Task<bool> CheckOutputWithCheckerAsync(string actualOutput, string inputFilePath, string expectedOutputPath, string? checkerPath);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ namespace LiquidCode.Tester.Worker.Services;
|
||||
public class OutputCheckerService : IOutputCheckerService
|
||||
{
|
||||
private readonly ILogger<OutputCheckerService> _logger;
|
||||
private readonly CheckerService _checkerService;
|
||||
|
||||
public OutputCheckerService(ILogger<OutputCheckerService> logger)
|
||||
public OutputCheckerService(ILogger<OutputCheckerService> logger, CheckerService checkerService)
|
||||
{
|
||||
_logger = logger;
|
||||
_checkerService = checkerService;
|
||||
}
|
||||
|
||||
public async Task<bool> CheckOutputAsync(string actualOutput, string expectedOutputPath)
|
||||
@@ -51,4 +53,35 @@ public class OutputCheckerService : IOutputCheckerService
|
||||
|
||||
return string.Join("\n", lines);
|
||||
}
|
||||
|
||||
public async Task<bool> CheckOutputWithCheckerAsync(
|
||||
string actualOutput,
|
||||
string inputFilePath,
|
||||
string expectedOutputPath,
|
||||
string? checkerPath)
|
||||
{
|
||||
// If custom checker is available, use it
|
||||
if (!string.IsNullOrEmpty(checkerPath) && File.Exists(checkerPath))
|
||||
{
|
||||
_logger.LogDebug("Using custom checker: {CheckerPath}", checkerPath);
|
||||
|
||||
var checkerResult = await _checkerService.CheckAsync(
|
||||
checkerPath,
|
||||
inputFilePath,
|
||||
actualOutput,
|
||||
expectedOutputPath);
|
||||
|
||||
if (!checkerResult.Accepted)
|
||||
{
|
||||
_logger.LogWarning("Custom checker verdict: {Verdict} - {Message}",
|
||||
checkerResult.Verdict, checkerResult.Message);
|
||||
}
|
||||
|
||||
return checkerResult.Accepted;
|
||||
}
|
||||
|
||||
// Fall back to standard string comparison
|
||||
_logger.LogDebug("No custom checker, using standard comparison");
|
||||
return await CheckOutputAsync(actualOutput, expectedOutputPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,20 @@ namespace LiquidCode.Tester.Worker.Services;
|
||||
public class PackageParserService : IPackageParserService
|
||||
{
|
||||
private readonly ILogger<PackageParserService> _logger;
|
||||
private readonly PolygonProblemXmlParser _polygonParser;
|
||||
private readonly AnswerGenerationService _answerGenerator;
|
||||
private readonly CppCompilationService _cppCompilation;
|
||||
|
||||
public PackageParserService(ILogger<PackageParserService> logger)
|
||||
public PackageParserService(
|
||||
ILogger<PackageParserService> logger,
|
||||
PolygonProblemXmlParser polygonParser,
|
||||
AnswerGenerationService answerGenerator,
|
||||
CppCompilationService cppCompilation)
|
||||
{
|
||||
_logger = logger;
|
||||
_polygonParser = polygonParser;
|
||||
_answerGenerator = answerGenerator;
|
||||
_cppCompilation = cppCompilation;
|
||||
}
|
||||
|
||||
public async Task<ProblemPackage> ParsePackageAsync(Stream packageStream)
|
||||
@@ -25,6 +35,132 @@ public class PackageParserService : IPackageParserService
|
||||
using var archive = new ZipArchive(packageStream, ZipArchiveMode.Read);
|
||||
archive.ExtractToDirectory(workingDirectory);
|
||||
|
||||
// Check if this is a Polygon package (has problem.xml)
|
||||
var problemXmlPath = Path.Combine(workingDirectory, "problem.xml");
|
||||
if (File.Exists(problemXmlPath))
|
||||
{
|
||||
_logger.LogInformation("Detected Polygon package format (problem.xml found)");
|
||||
return await ParsePolygonPackageAsync(workingDirectory, problemXmlPath);
|
||||
}
|
||||
|
||||
// Fall back to legacy format (.in/.out files)
|
||||
_logger.LogInformation("Using legacy package format (.in/.out files)");
|
||||
return await ParseLegacyPackage(workingDirectory);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to parse package");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ProblemPackage> ParsePolygonPackageAsync(string workingDirectory, string problemXmlPath)
|
||||
{
|
||||
var descriptor = _polygonParser.ParseProblemXml(problemXmlPath);
|
||||
|
||||
if (descriptor == null)
|
||||
{
|
||||
_logger.LogWarning("Failed to parse problem.xml, falling back to legacy format");
|
||||
return await ParseLegacyPackage(workingDirectory);
|
||||
}
|
||||
|
||||
var package = new ProblemPackage
|
||||
{
|
||||
WorkingDirectory = workingDirectory,
|
||||
DefaultTimeLimit = descriptor.TimeLimitMs,
|
||||
DefaultMemoryLimit = descriptor.MemoryLimitMb
|
||||
};
|
||||
|
||||
// Collect test file paths and check which answers are missing
|
||||
var inputPaths = new List<string>();
|
||||
var answerPaths = new List<string>();
|
||||
var missingAnswerPaths = new List<string>();
|
||||
var missingAnswerInputs = new List<string>();
|
||||
|
||||
for (int i = 1; i <= descriptor.TestCount; i++)
|
||||
{
|
||||
var inputPath = Path.Combine(workingDirectory,
|
||||
string.Format(descriptor.InputPathPattern.Replace("%02d", "{0:D2}"), i));
|
||||
var answerPath = Path.Combine(workingDirectory,
|
||||
string.Format(descriptor.AnswerPathPattern.Replace("%02d", "{0:D2}"), i));
|
||||
|
||||
if (!File.Exists(inputPath))
|
||||
{
|
||||
_logger.LogWarning("Input file not found: {InputPath}", inputPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
inputPaths.Add(inputPath);
|
||||
answerPaths.Add(answerPath);
|
||||
|
||||
if (!File.Exists(answerPath))
|
||||
{
|
||||
missingAnswerPaths.Add(answerPath);
|
||||
missingAnswerInputs.Add(inputPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate missing answer files if we have a main solution
|
||||
if (missingAnswerPaths.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Found {Count} tests without answer files, attempting to generate them",
|
||||
missingAnswerPaths.Count);
|
||||
|
||||
var generated = await _answerGenerator.GenerateAnswersAsync(
|
||||
descriptor,
|
||||
workingDirectory,
|
||||
missingAnswerInputs,
|
||||
missingAnswerPaths);
|
||||
|
||||
if (generated)
|
||||
{
|
||||
_logger.LogInformation("Successfully generated answer files");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Failed to generate answer files, tests without answers will be skipped");
|
||||
}
|
||||
}
|
||||
|
||||
// Now create test cases for all tests that have answer files
|
||||
for (int i = 0; i < inputPaths.Count; i++)
|
||||
{
|
||||
var inputPath = inputPaths[i];
|
||||
var answerPath = answerPaths[i];
|
||||
|
||||
if (!File.Exists(answerPath))
|
||||
{
|
||||
_logger.LogWarning("Answer file not found: {AnswerPath} (skipping test)", answerPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
package.TestCases.Add(new TestCase
|
||||
{
|
||||
Number = i + 1,
|
||||
InputFilePath = inputPath,
|
||||
OutputFilePath = answerPath,
|
||||
TimeLimit = descriptor.TimeLimitMs,
|
||||
MemoryLimit = descriptor.MemoryLimitMb
|
||||
});
|
||||
}
|
||||
|
||||
// Look for and compile checker
|
||||
package.CheckerPath = await FindAndCompileCheckerAsync(workingDirectory);
|
||||
|
||||
if (package.TestCases.Count == 0)
|
||||
{
|
||||
_logger.LogWarning("No test cases with answer files found! Expected format: {InputPattern} -> {AnswerPattern}",
|
||||
descriptor.InputPathPattern, descriptor.AnswerPathPattern);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Parsed Polygon package with {TestCount} tests (out of {TotalTests} in problem.xml)",
|
||||
package.TestCases.Count, descriptor.TestCount);
|
||||
|
||||
return package;
|
||||
}
|
||||
|
||||
private async Task<ProblemPackage> ParseLegacyPackage(string workingDirectory)
|
||||
{
|
||||
var package = new ProblemPackage
|
||||
{
|
||||
WorkingDirectory = workingDirectory
|
||||
@@ -65,27 +201,17 @@ public class PackageParserService : IPackageParserService
|
||||
});
|
||||
}
|
||||
|
||||
// Look for checker
|
||||
var checkerCandidates = new[] { "check.cpp", "checker.cpp", "check", "checker" };
|
||||
foreach (var candidate in checkerCandidates)
|
||||
// Look for and compile checker
|
||||
package.CheckerPath = await FindAndCompileCheckerAsync(workingDirectory);
|
||||
|
||||
if (package.TestCases.Count == 0)
|
||||
{
|
||||
var checkerPath = Path.Combine(workingDirectory, candidate);
|
||||
if (File.Exists(checkerPath))
|
||||
{
|
||||
package.CheckerPath = checkerPath;
|
||||
break;
|
||||
}
|
||||
_logger.LogWarning("No test cases found! Check package structure. Expected .in/.out files in tests directory or root");
|
||||
}
|
||||
|
||||
_logger.LogInformation("Parsed package with {TestCount} tests", package.TestCases.Count);
|
||||
_logger.LogInformation("Parsed legacy package with {TestCount} tests", package.TestCases.Count);
|
||||
return package;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to parse package");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private string? FindCorrespondingOutputFile(string inputFile)
|
||||
{
|
||||
@@ -117,4 +243,68 @@ public class PackageParserService : IPackageParserService
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<string?> FindAndCompileCheckerAsync(string workingDirectory)
|
||||
{
|
||||
// Try to find checker in common locations
|
||||
var checkerCandidates = new[]
|
||||
{
|
||||
Path.Combine(workingDirectory, "check.exe"),
|
||||
Path.Combine(workingDirectory, "checker.exe"),
|
||||
Path.Combine(workingDirectory, "check.cpp"),
|
||||
Path.Combine(workingDirectory, "checker.cpp"),
|
||||
Path.Combine(workingDirectory, "files", "check.exe"),
|
||||
Path.Combine(workingDirectory, "files", "checker.exe"),
|
||||
Path.Combine(workingDirectory, "files", "check.cpp"),
|
||||
Path.Combine(workingDirectory, "files", "checker.cpp")
|
||||
};
|
||||
|
||||
foreach (var candidate in checkerCandidates)
|
||||
{
|
||||
if (!File.Exists(candidate))
|
||||
continue;
|
||||
|
||||
// If it's already an executable, use it
|
||||
if (candidate.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogInformation("Found checker executable: {CheckerPath}", candidate);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
// If it's C++ source, compile it
|
||||
if (candidate.EndsWith(".cpp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogInformation("Found checker source: {CheckerPath}, compiling...", candidate);
|
||||
|
||||
try
|
||||
{
|
||||
var checkerSource = await File.ReadAllTextAsync(candidate);
|
||||
var checkerDir = Path.GetDirectoryName(candidate)!;
|
||||
|
||||
// Compile checker with C++17 (testlib.h compatible)
|
||||
var compilationResult = await _cppCompilation.CompileAsync(
|
||||
checkerSource,
|
||||
checkerDir,
|
||||
"17");
|
||||
|
||||
if (!compilationResult.Success)
|
||||
{
|
||||
_logger.LogError("Failed to compile checker: {Error}", compilationResult.CompilerOutput);
|
||||
continue; // Try next candidate
|
||||
}
|
||||
|
||||
_logger.LogInformation("Checker compiled successfully: {ExecutablePath}", compilationResult.ExecutablePath);
|
||||
return compilationResult.ExecutablePath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error compiling checker: {CheckerPath}", candidate);
|
||||
continue; // Try next candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogWarning("No checker found in package");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
141
src/LiquidCode.Tester.Worker/Services/PolygonProblemXmlParser.cs
Normal file
141
src/LiquidCode.Tester.Worker/Services/PolygonProblemXmlParser.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
using System.Xml.Linq;
|
||||
using LiquidCode.Tester.Common.Models;
|
||||
|
||||
namespace LiquidCode.Tester.Worker.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Parser for Polygon problem.xml format
|
||||
/// </summary>
|
||||
public class PolygonProblemXmlParser
|
||||
{
|
||||
private readonly ILogger<PolygonProblemXmlParser> _logger;
|
||||
|
||||
public PolygonProblemXmlParser(ILogger<PolygonProblemXmlParser> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public PolygonProblemDescriptor? ParseProblemXml(string xmlPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var doc = XDocument.Load(xmlPath);
|
||||
var problem = doc.Element("problem");
|
||||
|
||||
if (problem == null)
|
||||
{
|
||||
_logger.LogWarning("Invalid problem.xml: root 'problem' element not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
var judging = problem.Element("judging");
|
||||
if (judging == null)
|
||||
{
|
||||
_logger.LogWarning("No 'judging' section found in problem.xml");
|
||||
return null;
|
||||
}
|
||||
|
||||
var testset = judging.Element("testset");
|
||||
if (testset == null)
|
||||
{
|
||||
_logger.LogWarning("No 'testset' section found in problem.xml");
|
||||
return null;
|
||||
}
|
||||
|
||||
var descriptor = new PolygonProblemDescriptor
|
||||
{
|
||||
ShortName = problem.Attribute("short-name")?.Value ?? "unknown",
|
||||
Revision = int.TryParse(problem.Attribute("revision")?.Value, out var rev) ? rev : 0
|
||||
};
|
||||
|
||||
// Parse time limit (in milliseconds)
|
||||
var timeLimitText = testset.Element("time-limit")?.Value;
|
||||
if (int.TryParse(timeLimitText, out var timeLimit))
|
||||
{
|
||||
descriptor.TimeLimitMs = timeLimit;
|
||||
}
|
||||
|
||||
// Parse memory limit (in bytes)
|
||||
var memoryLimitText = testset.Element("memory-limit")?.Value;
|
||||
if (long.TryParse(memoryLimitText, out var memoryLimit))
|
||||
{
|
||||
descriptor.MemoryLimitMb = (int)(memoryLimit / (1024 * 1024)); // Convert bytes to MB
|
||||
}
|
||||
|
||||
// Parse test count
|
||||
var testCountText = testset.Element("test-count")?.Value;
|
||||
if (int.TryParse(testCountText, out var testCount))
|
||||
{
|
||||
descriptor.TestCount = testCount;
|
||||
}
|
||||
|
||||
// Parse path patterns
|
||||
descriptor.InputPathPattern = testset.Element("input-path-pattern")?.Value ?? "tests/%02d";
|
||||
descriptor.AnswerPathPattern = testset.Element("answer-path-pattern")?.Value ?? "tests/%02d.a";
|
||||
|
||||
// Parse solutions to find main solution
|
||||
var assets = problem.Element("assets");
|
||||
if (assets != null)
|
||||
{
|
||||
var solutions = assets.Element("solutions");
|
||||
if (solutions != null)
|
||||
{
|
||||
// Try to find main solution first
|
||||
var mainSolution = solutions.Elements("solution")
|
||||
.FirstOrDefault(s => s.Attribute("tag")?.Value == "main");
|
||||
|
||||
// If no main solution, try to find any accepted solution
|
||||
if (mainSolution == null)
|
||||
{
|
||||
mainSolution = solutions.Elements("solution")
|
||||
.FirstOrDefault(s => s.Attribute("tag")?.Value == "accepted");
|
||||
}
|
||||
|
||||
if (mainSolution != null)
|
||||
{
|
||||
var source = mainSolution.Element("source");
|
||||
if (source != null)
|
||||
{
|
||||
descriptor.MainSolutionPath = source.Attribute("path")?.Value;
|
||||
descriptor.MainSolutionType = source.Attribute("type")?.Value;
|
||||
|
||||
_logger.LogInformation("Found main solution: {Path} (type: {Type})",
|
||||
descriptor.MainSolutionPath, descriptor.MainSolutionType);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("No main or accepted solution found in problem.xml");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Parsed problem.xml: {ShortName} (rev {Revision}), {TestCount} tests, TL={TimeLimit}ms, ML={MemoryLimit}MB",
|
||||
descriptor.ShortName, descriptor.Revision, descriptor.TestCount, descriptor.TimeLimitMs, descriptor.MemoryLimitMb);
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to parse problem.xml at {Path}", xmlPath);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Descriptor parsed from problem.xml
|
||||
/// </summary>
|
||||
public class PolygonProblemDescriptor
|
||||
{
|
||||
public string ShortName { get; set; } = string.Empty;
|
||||
public int Revision { get; set; }
|
||||
public int TimeLimitMs { get; set; } = 2000;
|
||||
public int MemoryLimitMb { get; set; } = 256;
|
||||
public int TestCount { get; set; }
|
||||
public string InputPathPattern { get; set; } = "tests/%02d";
|
||||
public string AnswerPathPattern { get; set; } = "tests/%02d.a";
|
||||
public string? MainSolutionPath { get; set; }
|
||||
public string? MainSolutionType { get; set; }
|
||||
}
|
||||
@@ -58,6 +58,16 @@ public class TestingService : ITestingService
|
||||
|
||||
_logger.LogInformation("Package parsed, found {TestCount} tests", package.TestCases.Count);
|
||||
|
||||
// Validate that package contains test cases
|
||||
if (package.TestCases.Count == 0)
|
||||
{
|
||||
_logger.LogError("No test cases found in package for submit {SubmitId}", request.Id);
|
||||
await SendStatusAsync(request, State.Done, ErrorCode.UnknownError,
|
||||
"No test cases found in package", 0, 0);
|
||||
CleanupWorkingDirectory(package.WorkingDirectory);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send compiling status
|
||||
await SendStatusAsync(request, State.Compiling, ErrorCode.None, "Compiling solution", 0, package.TestCases.Count);
|
||||
|
||||
@@ -125,8 +135,12 @@ public class TestingService : ITestingService
|
||||
return;
|
||||
}
|
||||
|
||||
// Check output
|
||||
var outputCorrect = await _outputChecker.CheckOutputAsync(executionResult.Output, testCase.OutputFilePath);
|
||||
// Check output (using custom checker if available)
|
||||
var outputCorrect = await _outputChecker.CheckOutputWithCheckerAsync(
|
||||
executionResult.Output,
|
||||
testCase.InputFilePath,
|
||||
testCase.OutputFilePath,
|
||||
package.CheckerPath);
|
||||
|
||||
if (!outputCorrect)
|
||||
{
|
||||
|
||||
@@ -12,7 +12,9 @@ public class OutputCheckerServiceTests : IDisposable
|
||||
public OutputCheckerServiceTests()
|
||||
{
|
||||
var logger = new Mock<ILogger<OutputCheckerService>>();
|
||||
_service = new OutputCheckerService(logger.Object);
|
||||
var checkerLogger = new Mock<ILogger<CheckerService>>();
|
||||
var checkerService = new CheckerService(checkerLogger.Object);
|
||||
_service = new OutputCheckerService(logger.Object, checkerService);
|
||||
_testDirectory = Path.Combine(Path.GetTempPath(), "OutputCheckerTests", Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(_testDirectory);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,23 @@ public class PackageParserServiceTests : IDisposable
|
||||
public PackageParserServiceTests()
|
||||
{
|
||||
var logger = new Mock<ILogger<PackageParserService>>();
|
||||
_service = new PackageParserService(logger.Object);
|
||||
var xmlLogger = new Mock<ILogger<PolygonProblemXmlParser>>();
|
||||
var answerGenLogger = new Mock<ILogger<AnswerGenerationService>>();
|
||||
var cppLogger = new Mock<ILogger<CppCompilationService>>();
|
||||
var cppConfigMock = new Mock<Microsoft.Extensions.Configuration.IConfiguration>();
|
||||
|
||||
var polygonParser = new PolygonProblemXmlParser(xmlLogger.Object);
|
||||
|
||||
var compilationFactory = new Mock<ICompilationServiceFactory>();
|
||||
var executionFactory = new Mock<IExecutionServiceFactory>();
|
||||
var answerGenerator = new AnswerGenerationService(
|
||||
compilationFactory.Object,
|
||||
executionFactory.Object,
|
||||
answerGenLogger.Object);
|
||||
|
||||
var cppCompilation = new CppCompilationService(cppLogger.Object, cppConfigMock.Object);
|
||||
|
||||
_service = new PackageParserService(logger.Object, polygonParser, answerGenerator, cppCompilation);
|
||||
_testDirectory = Path.Combine(Path.GetTempPath(), "PackageParserTests", Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(_testDirectory);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
using System.IO.Compression;
|
||||
using LiquidCode.Tester.Worker.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
|
||||
namespace LiquidCode.Tester.Worker.Tests;
|
||||
|
||||
public class PolygonPackageParserTests : IDisposable
|
||||
{
|
||||
private readonly PackageParserService _service;
|
||||
private readonly string _testDirectory;
|
||||
|
||||
public PolygonPackageParserTests()
|
||||
{
|
||||
var logger = new Mock<ILogger<PackageParserService>>();
|
||||
var xmlLogger = new Mock<ILogger<PolygonProblemXmlParser>>();
|
||||
var answerGenLogger = new Mock<ILogger<AnswerGenerationService>>();
|
||||
var cppLogger = new Mock<ILogger<CppCompilationService>>();
|
||||
var cppConfigMock = new Mock<Microsoft.Extensions.Configuration.IConfiguration>();
|
||||
|
||||
var polygonParser = new PolygonProblemXmlParser(xmlLogger.Object);
|
||||
|
||||
var compilationFactory = new Mock<ICompilationServiceFactory>();
|
||||
var executionFactory = new Mock<IExecutionServiceFactory>();
|
||||
var answerGenerator = new AnswerGenerationService(
|
||||
compilationFactory.Object,
|
||||
executionFactory.Object,
|
||||
answerGenLogger.Object);
|
||||
|
||||
var cppCompilation = new CppCompilationService(cppLogger.Object, cppConfigMock.Object);
|
||||
|
||||
_service = new PackageParserService(logger.Object, polygonParser, answerGenerator, cppCompilation);
|
||||
_testDirectory = Path.Combine(Path.GetTempPath(), "PolygonPackageTests", Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(_testDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParsePackageAsync_PolygonPackageWithProblemXml_ParsesSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var problemXml = @"<?xml version=""1.0"" encoding=""utf-8"" standalone=""no""?>
|
||||
<problem revision=""7"" short-name=""test-problem"">
|
||||
<judging>
|
||||
<testset name=""tests"">
|
||||
<time-limit>1000</time-limit>
|
||||
<memory-limit>268435456</memory-limit>
|
||||
<test-count>2</test-count>
|
||||
<input-path-pattern>tests/%02d</input-path-pattern>
|
||||
<answer-path-pattern>tests/%02d.a</answer-path-pattern>
|
||||
</testset>
|
||||
</judging>
|
||||
</problem>";
|
||||
|
||||
var zipStream = CreatePolygonPackage(problemXml, new[]
|
||||
{
|
||||
("tests/01", "input1"),
|
||||
("tests/01.a", "output1"),
|
||||
("tests/02", "input2"),
|
||||
("tests/02.a", "output2")
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _service.ParsePackageAsync(zipStream);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(2, result.TestCases.Count);
|
||||
Assert.Equal(1000, result.DefaultTimeLimit);
|
||||
Assert.Equal(256, result.DefaultMemoryLimit);
|
||||
|
||||
// Verify first test
|
||||
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));
|
||||
Assert.Equal("output1", await File.ReadAllTextAsync(result.TestCases[0].OutputFilePath));
|
||||
|
||||
// Verify second test
|
||||
Assert.Equal(2, result.TestCases[1].Number);
|
||||
Assert.True(File.Exists(result.TestCases[1].InputFilePath));
|
||||
Assert.True(File.Exists(result.TestCases[1].OutputFilePath));
|
||||
|
||||
// Cleanup
|
||||
if (Directory.Exists(result.WorkingDirectory))
|
||||
{
|
||||
Directory.Delete(result.WorkingDirectory, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParsePackageAsync_PolygonPackageMissingAnswerFiles_SkipsTests()
|
||||
{
|
||||
// Arrange
|
||||
var problemXml = @"<?xml version=""1.0"" encoding=""utf-8"" standalone=""no""?>
|
||||
<problem revision=""7"" short-name=""test-problem"">
|
||||
<judging>
|
||||
<testset name=""tests"">
|
||||
<time-limit>2000</time-limit>
|
||||
<memory-limit>536870912</memory-limit>
|
||||
<test-count>3</test-count>
|
||||
<input-path-pattern>tests/%02d</input-path-pattern>
|
||||
<answer-path-pattern>tests/%02d.a</answer-path-pattern>
|
||||
</testset>
|
||||
</judging>
|
||||
</problem>";
|
||||
|
||||
var zipStream = CreatePolygonPackage(problemXml, new[]
|
||||
{
|
||||
("tests/01", "input1"),
|
||||
("tests/01.a", "output1"),
|
||||
("tests/02", "input2"),
|
||||
// Missing 02.a
|
||||
("tests/03", "input3"),
|
||||
("tests/03.a", "output3")
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _service.ParsePackageAsync(zipStream);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(2, result.TestCases.Count); // Only tests with answer files
|
||||
Assert.Equal(1, result.TestCases[0].Number);
|
||||
Assert.Equal(3, result.TestCases[1].Number); // Test 2 skipped
|
||||
|
||||
// Cleanup
|
||||
if (Directory.Exists(result.WorkingDirectory))
|
||||
{
|
||||
Directory.Delete(result.WorkingDirectory, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParsePackageAsync_NoProblemXml_FallsBackToLegacyFormat()
|
||||
{
|
||||
// Arrange - create package without problem.xml
|
||||
var zipStream = CreateLegacyPackage(new[]
|
||||
{
|
||||
("tests/test1.in", "input1"),
|
||||
("tests/test1.out", "output1"),
|
||||
("tests/test2.in", "input2"),
|
||||
("tests/test2.out", "output2")
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _service.ParsePackageAsync(zipStream);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(2, result.TestCases.Count);
|
||||
Assert.Equal(2000, result.DefaultTimeLimit); // Default values
|
||||
Assert.Equal(256, result.DefaultMemoryLimit);
|
||||
|
||||
// Cleanup
|
||||
if (Directory.Exists(result.WorkingDirectory))
|
||||
{
|
||||
Directory.Delete(result.WorkingDirectory, true);
|
||||
}
|
||||
}
|
||||
|
||||
private MemoryStream CreatePolygonPackage(string problemXml, IEnumerable<(string fileName, string content)> files)
|
||||
{
|
||||
var memoryStream = new MemoryStream();
|
||||
|
||||
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
|
||||
{
|
||||
// Add problem.xml
|
||||
var xmlEntry = archive.CreateEntry("problem.xml");
|
||||
using var xmlStream = xmlEntry.Open();
|
||||
using var xmlWriter = new StreamWriter(xmlStream);
|
||||
xmlWriter.Write(problemXml);
|
||||
|
||||
// Add test files
|
||||
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;
|
||||
}
|
||||
|
||||
private MemoryStream CreateLegacyPackage(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))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(_testDirectory, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
211
tests/LiquidCode.Tester.Worker.Tests/TestingServiceTests.cs
Normal file
211
tests/LiquidCode.Tester.Worker.Tests/TestingServiceTests.cs
Normal file
@@ -0,0 +1,211 @@
|
||||
using System.IO.Compression;
|
||||
using LiquidCode.Tester.Common.Models;
|
||||
using LiquidCode.Tester.Worker.Controllers;
|
||||
using LiquidCode.Tester.Worker.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
|
||||
namespace LiquidCode.Tester.Worker.Tests;
|
||||
|
||||
public class TestingServiceTests : IDisposable
|
||||
{
|
||||
private readonly Mock<IPackageParserService> _packageParserMock;
|
||||
private readonly Mock<ICompilationServiceFactory> _compilationFactoryMock;
|
||||
private readonly Mock<IExecutionServiceFactory> _executionFactoryMock;
|
||||
private readonly Mock<IOutputCheckerService> _outputCheckerMock;
|
||||
private readonly Mock<ICallbackService> _callbackServiceMock;
|
||||
private readonly Mock<ILogger<TestingService>> _loggerMock;
|
||||
private readonly TestingService _service;
|
||||
private readonly string _testDirectory;
|
||||
|
||||
public TestingServiceTests()
|
||||
{
|
||||
_packageParserMock = new Mock<IPackageParserService>();
|
||||
_compilationFactoryMock = new Mock<ICompilationServiceFactory>();
|
||||
_executionFactoryMock = new Mock<IExecutionServiceFactory>();
|
||||
_outputCheckerMock = new Mock<IOutputCheckerService>();
|
||||
_callbackServiceMock = new Mock<ICallbackService>();
|
||||
_loggerMock = new Mock<ILogger<TestingService>>();
|
||||
|
||||
_service = new TestingService(
|
||||
_packageParserMock.Object,
|
||||
_compilationFactoryMock.Object,
|
||||
_executionFactoryMock.Object,
|
||||
_outputCheckerMock.Object,
|
||||
_callbackServiceMock.Object,
|
||||
_loggerMock.Object
|
||||
);
|
||||
|
||||
_testDirectory = Path.Combine(Path.GetTempPath(), "TestingServiceTests", Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(_testDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessSubmitAsync_EmptyPackage_ReturnsUnknownError()
|
||||
{
|
||||
// Arrange
|
||||
var packageFilePath = Path.Combine(_testDirectory, "empty_package.zip");
|
||||
await CreateEmptyPackage(packageFilePath);
|
||||
|
||||
var request = new TestRequest
|
||||
{
|
||||
Id = 123,
|
||||
MissionId = 456,
|
||||
Language = "cpp",
|
||||
LanguageVersion = "17",
|
||||
SourceCode = "int main() { return 0; }",
|
||||
PackageFilePath = packageFilePath,
|
||||
CallbackUrl = "http://localhost/callback"
|
||||
};
|
||||
|
||||
var emptyPackage = new ProblemPackage
|
||||
{
|
||||
WorkingDirectory = _testDirectory,
|
||||
TestCases = new List<TestCase>() // Empty list!
|
||||
};
|
||||
|
||||
_packageParserMock
|
||||
.Setup(x => x.ParsePackageAsync(It.IsAny<Stream>()))
|
||||
.ReturnsAsync(emptyPackage);
|
||||
|
||||
// Act
|
||||
await _service.ProcessSubmitAsync(request);
|
||||
|
||||
// Assert - verify callback was called with error
|
||||
_callbackServiceMock.Verify(
|
||||
x => x.SendStatusAsync(
|
||||
request.CallbackUrl,
|
||||
It.Is<TesterResponseModel>(r =>
|
||||
r.State == State.Done &&
|
||||
r.ErrorCode == ErrorCode.UnknownError &&
|
||||
r.Message == "No test cases found in package"
|
||||
)
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
|
||||
// Verify compilation was NOT attempted
|
||||
_compilationFactoryMock.Verify(
|
||||
x => x.GetCompilationService(It.IsAny<string>()),
|
||||
Times.Never
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessSubmitAsync_ValidPackage_RunsAllTests()
|
||||
{
|
||||
// Arrange
|
||||
var packageFilePath = Path.Combine(_testDirectory, "valid_package.zip");
|
||||
var inputFile = Path.Combine(_testDirectory, "1.in");
|
||||
var outputFile = Path.Combine(_testDirectory, "1.out");
|
||||
var executablePath = Path.Combine(_testDirectory, "solution.exe");
|
||||
|
||||
await File.WriteAllTextAsync(inputFile, "test input");
|
||||
await File.WriteAllTextAsync(outputFile, "expected output");
|
||||
await File.WriteAllTextAsync(executablePath, "dummy");
|
||||
|
||||
var request = new TestRequest
|
||||
{
|
||||
Id = 123,
|
||||
MissionId = 456,
|
||||
Language = "cpp",
|
||||
LanguageVersion = "17",
|
||||
SourceCode = "int main() { return 0; }",
|
||||
PackageFilePath = packageFilePath,
|
||||
CallbackUrl = "http://localhost/callback"
|
||||
};
|
||||
|
||||
var package = new ProblemPackage
|
||||
{
|
||||
WorkingDirectory = _testDirectory,
|
||||
TestCases = new List<TestCase>
|
||||
{
|
||||
new TestCase
|
||||
{
|
||||
Number = 1,
|
||||
InputFilePath = inputFile,
|
||||
OutputFilePath = outputFile,
|
||||
TimeLimit = 2000,
|
||||
MemoryLimit = 256
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var compilationService = new Mock<ICompilationService>();
|
||||
var executionService = new Mock<IExecutionService>();
|
||||
|
||||
_packageParserMock
|
||||
.Setup(x => x.ParsePackageAsync(It.IsAny<Stream>()))
|
||||
.ReturnsAsync(package);
|
||||
|
||||
_compilationFactoryMock
|
||||
.Setup(x => x.GetCompilationService("cpp"))
|
||||
.Returns(compilationService.Object);
|
||||
|
||||
_executionFactoryMock
|
||||
.Setup(x => x.GetExecutionService("cpp"))
|
||||
.Returns(executionService.Object);
|
||||
|
||||
compilationService
|
||||
.Setup(x => x.CompileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
|
||||
.ReturnsAsync(new CompilationResult
|
||||
{
|
||||
Success = true,
|
||||
ExecutablePath = executablePath
|
||||
});
|
||||
|
||||
executionService
|
||||
.Setup(x => x.ExecuteAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
|
||||
.ReturnsAsync(new ExecutionResult
|
||||
{
|
||||
Success = true,
|
||||
Output = "expected output",
|
||||
ExitCode = 0,
|
||||
RuntimeError = false,
|
||||
TimeLimitExceeded = false,
|
||||
MemoryLimitExceeded = false
|
||||
});
|
||||
|
||||
_outputCheckerMock
|
||||
.Setup(x => x.CheckOutputAsync(It.IsAny<string>(), It.IsAny<string>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
// Act
|
||||
await _service.ProcessSubmitAsync(request);
|
||||
|
||||
// Assert - verify callback was called with success
|
||||
_callbackServiceMock.Verify(
|
||||
x => x.SendStatusAsync(
|
||||
request.CallbackUrl,
|
||||
It.Is<TesterResponseModel>(r =>
|
||||
r.State == State.Done &&
|
||||
r.ErrorCode == ErrorCode.None &&
|
||||
r.Message == "All tests passed"
|
||||
)
|
||||
),
|
||||
Times.AtLeastOnce
|
||||
);
|
||||
}
|
||||
|
||||
private async Task CreateEmptyPackage(string filePath)
|
||||
{
|
||||
using var fileStream = File.Create(filePath);
|
||||
using var archive = new ZipArchive(fileStream, ZipArchiveMode.Create);
|
||||
// Create empty archive
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_testDirectory))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(_testDirectory, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore cleanup errors in tests
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user