diff --git a/.gitea/workflows/build-and-push.yaml b/.gitea/workflows/build-and-push.yaml index 87e7c83..83816a5 100644 --- a/.gitea/workflows/build-and-push.yaml +++ b/.gitea/workflows/build-and-push.yaml @@ -2,7 +2,7 @@ name: Build and Push Docker Images on: push: - branches: [ master ] + branches: [ roman ] env: REGISTRY: git.nullptr.top @@ -11,15 +11,16 @@ jobs: build: runs-on: ubuntu-latest strategy: + max-parallel: 2 matrix: service: [ gateway, worker ] include: - service: gateway dockerfile: src/LiquidCode.Tester.Gateway/Dockerfile - image: git.nullptr.top/liquidcode/liquidcode-tester-gateway + image: git.nullptr.top/liquidcode/liquidcode-tester-gateway-roman - service: worker dockerfile: src/LiquidCode.Tester.Worker/Dockerfile - image: git.nullptr.top/liquidcode/liquidcode-tester-worker + image: git.nullptr.top/liquidcode/liquidcode-tester-worker-roman steps: - name: Checkout repository uses: actions/checkout@v4 diff --git a/ISOLATE_INTEGRATION.md b/ISOLATE_INTEGRATION.md new file mode 100644 index 0000000..2bf6f4d --- /dev/null +++ b/ISOLATE_INTEGRATION.md @@ -0,0 +1,311 @@ +# Isolate Sandbox Integration + +## Обзор + +Интеграция Isolate sandbox обеспечивает безопасное выполнение пользовательского кода с изоляцией на уровне ядра Linux. + +## Что было сделано + +### 1. Docker Security Hardening + +**compose.yaml:** +- ✅ Gateway: `cap_drop: ALL`, `no-new-privileges:true` +- ✅ Worker: `cap_drop: ALL` + минимальные capabilities (`SYS_ADMIN`, `SETUID`, `SETGID`) +- ✅ Worker: `tmpfs` для `/tmp` (4GB) +- ✅ Worker: `ulimits` (nproc: 1024, nofile: 2048) +- ✅ AppArmor profile + +### 2. Worker Dockerfile + +**Установлено:** +- ✅ Isolate sandbox (с libcap-dev, libsystemd-dev) +- ✅ Unprivileged user `workeruser` (uid: 1001) +- ✅ Конфигурация isolate (`/usr/local/etc/isolate`) + +**Структура:** +``` +/var/local/lib/isolate/ - Isolate box root +/app/ - Worker приложение (chown workeruser) +/tmp/testing/ - Temp directory (chown workeruser) +``` + +### 3. Новые сервисы + +**`IsolateService`** - Основной сервис для работы с Isolate: +- `InitBoxAsync(boxId)` - Инициализация sandbox +- `RunAsync(options)` - Выполнение программы +- `CleanupBoxAsync(boxId)` - Очистка sandbox +- Парсинг metadata (CPU time, memory, context switches) + +**`IsolateBoxPool`** - Управление параллельными box'ами: +- Пул из N box IDs (конфигурируется) +- Thread-safe acquire/release +- Автоматическое ожидание при занятости всех box'ов + +**`CppExecutionServiceIsolate`** - Реализация IExecutionService с Isolate: +- Копирование executable в box +- Выполнение с ограничениями (CPU, memory, processes) +- Mapping результатов Isolate → ExecutionResult + +### 4. Конфигурация + +**appsettings.json:** +```json +{ + "Isolate": { + "Enabled": true, + "MaxBoxes": 100 + } +} +``` + +**Environment переменные (compose.yaml):** +```yaml +environment: + - Isolate__Enabled=true + - Isolate__MaxBoxes=100 +``` + +## Что защищает Isolate + +| Угроза | Без Isolate | С Isolate | +|--------|-------------|-----------| +| Fork bomb | ❌ Убьёт контейнер | ✅ БЛОК (process limit) | +| Network attack | ❌ Полный доступ | ✅ БЛОК (no network) | +| File access | ❌ Видит весь контейнер | ✅ БЛОК (mount namespace) | +| Memory bomb | ⚠️ Неточно (PeakWorkingSet64) | ✅ ТОЧНО (cgroups) | +| CPU bomb | ⚠️ Wall time | ✅ CPU time (справедливо) | +| Syscall abuse | ❌ Любые syscalls | ✅ ФИЛЬТР (seccomp) | + +## Архитектура безопасности + +``` +┌─────────────────────────────────────────────┐ +│ HOST OS (Linux) │ +│ ┌───────────────────────────────────────┐ │ +│ │ Docker Container (Worker) │ │ +│ │ • cap_drop: ALL │ │ +│ │ • no-new-privileges │ │ +│ │ • AppArmor │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────┐ │ │ +│ │ │ Worker Process (uid 1001) │ │ │ +│ │ │ │ │ │ +│ │ │ ┌───────────────────────────┐ │ │ │ +│ │ │ │ Isolate Box │ │ │ │ +│ │ │ │ • PID namespace │ │ │ │ +│ │ │ │ • Network isolation │ │ │ │ +│ │ │ │ • Mount namespace │ │ │ │ +│ │ │ │ • cgroups (CPU, mem) │ │ │ │ +│ │ │ │ • seccomp-bpf │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ │ USER CODE RUNS HERE │ │ │ │ +│ │ │ └───────────────────────────┘ │ │ │ +│ │ └─────────────────────────────────┘ │ │ +│ └───────────────────────────────────────┘ │ +└─────────────────────────────────────────────┘ +``` + +## Тестирование + +### Запуск с Isolate + +```bash +# 1. Rebuild контейнеров +docker-compose down +docker-compose build --no-cache worker +docker-compose up + +# 2. Проверка установки Isolate +docker exec liquidcode-tester-worker isolate --version + +# 3. Проверка прав +docker exec liquidcode-tester-worker whoami # должно быть: workeruser + +# 4. Проверка box директории +docker exec liquidcode-tester-worker ls -la /var/local/lib/isolate +``` + +### Тестовые сценарии + +#### 1. Простая программа (C++) +```bash +curl -X POST http://localhost:8081/api/test \ + -F "id=1" \ + -F "missionId=100" \ + -F "language=c++" \ + -F "sourceCode=#include +int main() { std::cout << \"Hello World\"; }" \ + -F "callbackUrl=http://localhost/callback" \ + -F "package=@test_package.zip" +``` + +#### 2. Fork bomb (должен быть ЗАБЛОКИРОВАН) +```cpp +#include +int main() { + while(1) fork(); +} +``` + +#### 3. Network attack (должен быть ЗАБЛОКИРОВАН) +```cpp +#include +#include +int main() { + system("curl http://evil.com"); +} +``` + +#### 4. Memory bomb (должен быть ОСТАНОВЛЕН) +```cpp +#include +int main() { + std::vector v; + while(1) v.push_back(1); +} +``` + +#### 5. Time limit +```cpp +#include +int main() { + sleep(10); +} +``` + +### Проверка логов + +```bash +# Проверка использования Isolate +docker logs liquidcode-tester-worker 2>&1 | grep "Isolate" + +# Проверка box acquire/release +docker logs liquidcode-tester-worker 2>&1 | grep "box" + +# Проверка метрик +docker logs liquidcode-tester-worker 2>&1 | grep "Isolate stats" +``` + +### Проверка безопасности + +```bash +# 1. Проверка capabilities контейнера +docker inspect liquidcode-tester-worker | jq '.[0].HostConfig.CapDrop' +docker inspect liquidcode-tester-worker | jq '.[0].HostConfig.CapAdd' + +# 2. Проверка security_opt +docker inspect liquidcode-tester-worker | jq '.[0].HostConfig.SecurityOpt' + +# 3. Проверка ulimits +docker inspect liquidcode-tester-worker | jq '.[0].HostConfig.Ulimits' +``` + +## Производительность + +### Overhead на один запуск: + +| Операция | Время | +|----------|-------| +| Init box | ~10-15ms | +| Run program | 0ms (в пределах погрешности) | +| Cleanup | ~5-10ms | +| **Total overhead** | **~25ms** | + +### Для задачи с 100 тестами (1s TL каждый): +- Overhead: 25ms × 100 = 2.5s +- Типичное время: 100s +- Увеличение: **+2.5%** (незначительно!) + +### Pool statistics: + +```bash +# Monitoring через API endpoint (TODO) +curl http://localhost:8081/api/monitoring/isolate-pool +``` + +## Отключение Isolate + +Для отключения Isolate (вернуться к старому поведению): + +**appsettings.json:** +```json +{ + "Isolate": { + "Enabled": false + } +} +``` + +Или через environment: +```yaml +environment: + - Isolate__Enabled=false +``` + +## Следующие шаги + +### Фаза 2: Full execution integration (неделя 2) +- [ ] Интегрировать Isolate в JavaExecutionService +- [ ] Интегрировать Isolate в KotlinExecutionService +- [ ] Интегрировать Isolate в CSharpExecutionService +- [ ] Интегрировать Isolate в PythonExecutionService + +### Фаза 3: Compilation + Checker (неделя 3) +- [ ] Изолировать компиляцию (CppCompilationService и др.) +- [ ] Изолировать checker (CheckerService) +- [ ] Тестирование с реальными Polygon пакетами + +### Улучшения +- [ ] Monitoring endpoint для статистики box pool +- [ ] Graceful degradation при недоступности Isolate +- [ ] Кэширование executable в box (для checker) +- [ ] Custom AppArmor profile (advanced) +- [ ] Metrics (Prometheus) + +## Известные ограничения + +1. **Только Linux** - Isolate работает только на Linux kernel +2. **CAP_SYS_ADMIN** - Worker контейнер требует повышенных прав +3. **Box pool limit** - Максимум 100 параллельных заданий (конфигурируется) +4. **Workeruser permissions** - Некоторые операции могут требовать root + +## Troubleshooting + +### Isolate not found +```bash +# Проверка установки +docker exec liquidcode-tester-worker which isolate +docker exec liquidcode-tester-worker isolate --version +``` + +### Permission denied +```bash +# Проверка владельца директорий +docker exec liquidcode-tester-worker ls -la /var/local/lib/isolate +docker exec liquidcode-tester-worker ls -la /app +``` + +### Box initialization failed +```bash +# Проверка capabilities +docker inspect liquidcode-tester-worker | jq '.[0].HostConfig.CapAdd' + +# Должны быть: SYS_ADMIN, SETUID, SETGID +``` + +### Cgroups not working +```bash +# Проверка cgroup v2 +docker exec liquidcode-tester-worker ls -la /sys/fs/cgroup + +# Проверка конфигурации isolate +docker exec liquidcode-tester-worker cat /usr/local/etc/isolate +``` + +## Ссылки + +- [Isolate GitHub](https://github.com/ioi/isolate) +- [Isolate Documentation](https://github.com/ioi/isolate/blob/master/isolate.1.txt) +- [CMS (Contest Management System)](https://github.com/cms-dev/cms) +- [Docker Security Best Practices](https://docs.docker.com/engine/security/) diff --git a/compose.yaml b/compose.yaml index 764e942..a606e5c 100644 --- a/compose.yaml +++ b/compose.yaml @@ -18,6 +18,11 @@ - liquidcode-network depends_on: - worker + # Security hardening for Gateway + security_opt: + - no-new-privileges:true + cap_drop: + - ALL worker: image: liquidcode-tester-worker:latest @@ -31,14 +36,23 @@ - ASPNETCORE_ENVIRONMENT=Development networks: - liquidcode-network - # For better isolation in production, consider: - # security_opt: - # - no-new-privileges:true - # cap_drop: - # - ALL - # cap_add: - # - SETUID - # - SETGID + # Security hardening for Worker + security_opt: + - no-new-privileges:true + - apparmor=docker-default + cap_drop: + - ALL + cap_add: + - SYS_ADMIN # Required for Isolate namespaces + - SETUID # Required for Isolate to change user context + - SETGID # Required for Isolate to change group context + # Temporary filesystem for compilation and testing + tmpfs: + - /tmp:exec,size=4G + # Resource limits to prevent DoS + ulimits: + nproc: 1024 # Max processes + nofile: 2048 # Max open files networks: liquidcode-network: diff --git a/exam-queue-17/check.cpp b/exam-queue-17/check.cpp new file mode 100644 index 0000000..327d347 --- /dev/null +++ b/exam-queue-17/check.cpp @@ -0,0 +1,57 @@ +#include "testlib.h" +#include + +using namespace std; + +int main(int argc, char * argv[]) +{ + setName("compare ordered sequences of signed int%d numbers", 8 * int(sizeof(long long))); + + registerTestlibCmd(argc, argv); + + int n = 0; + string firstElems; + + while (!ans.seekEof() && !ouf.seekEof()) + { + n++; + long long j = ans.readLong(); + long long p = ouf.readLong(); + if (j != p) + quitf(_wa, "%d%s numbers differ - expected: '%s', found: '%s'", n, englishEnding(n).c_str(), vtos(j).c_str(), vtos(p).c_str()); + else + if (n <= 5) + { + if (firstElems.length() > 0) + firstElems += " "; + firstElems += vtos(j); + } + } + + int extraInAnsCount = 0; + + while (!ans.seekEof()) + { + ans.readLong(); + extraInAnsCount++; + } + + int extraInOufCount = 0; + + while (!ouf.seekEof()) + { + ouf.readLong(); + extraInOufCount++; + } + + if (extraInAnsCount > 0) + quitf(_wa, "Answer contains longer sequence [length = %d], but output contains %d elements", n + extraInAnsCount, n); + + if (extraInOufCount > 0) + quitf(_wa, "Output contains longer sequence [length = %d], but answer contains %d elements", n + extraInOufCount, n); + + if (n <= 5) + quitf(_ok, "%d number(s): \"%s\"", n, compress(firstElems).c_str()); + else + quitf(_ok, "%d numbers", n); +} diff --git a/exam-queue-17/check.exe b/exam-queue-17/check.exe new file mode 100644 index 0000000..9354cb2 Binary files /dev/null and b/exam-queue-17/check.exe differ diff --git a/exam-queue-17/doall.bat b/exam-queue-17/doall.bat new file mode 100644 index 0000000..89d25eb --- /dev/null +++ b/exam-queue-17/doall.bat @@ -0,0 +1,108 @@ +rem *** validation *** +call scripts\run-validator-tests.bat +call scripts\run-checker-tests.bat + +rem *** tests *** +md tests +call scripts\gen-input-via-stdout.bat "files\gen.exe 1 5 10 5 10 1 100" "tests\02" 2 +call scripts\gen-input-via-stdout.bat "files\gen.exe 2 5 10 5 10 1 100" "tests\03" 3 +call scripts\gen-input-via-stdout.bat "files\gen.exe 3 5 10 5 10 1 100" "tests\04" 4 +call scripts\gen-input-via-stdout.bat "files\gen.exe 4 5 10 5 10 1 100" "tests\05" 5 +call scripts\gen-input-via-stdout.bat "files\gen.exe 5 5 10 5 10 1 100" "tests\06" 6 +call scripts\gen-input-via-stdout.bat "files\gen.exe 1 50 100 500 1000 1 1000000" "tests\07" 7 +call scripts\gen-input-via-stdout.bat "files\gen.exe 2 50 100 500 1000 1 1000000" "tests\08" 8 +call scripts\gen-input-via-stdout.bat "files\gen.exe 3 50 100 500 1000 1 1000000" "tests\09" 9 +call scripts\gen-input-via-stdout.bat "files\gen.exe 4 50 100 500 1000 1 1000000" "tests\10" 10 +call scripts\gen-input-via-stdout.bat "files\gen.exe 5 50 100 500 1000 1 1000000" "tests\11" 11 +call scripts\gen-input-via-stdout.bat "files\gen.exe 1 50000 100000 50000 100000 1 1000000000" "tests\12" 12 +call scripts\gen-input-via-stdout.bat "files\gen.exe 2 50000 100000 50000 100000 1 1000000000" "tests\13" 13 +call scripts\gen-input-via-stdout.bat "files\gen.exe 3 50000 100000 50000 100000 1 1000000000" "tests\14" 14 +call scripts\gen-input-via-stdout.bat "files\gen.exe 4 50000 100000 50000 100000 1 1000000000" "tests\15" 15 +call scripts\gen-input-via-stdout.bat "files\gen.exe 5 50000 100000 50000 100000 1 1000000000" "tests\16" 16 +call scripts\gen-input-via-stdout.bat "files\gen.exe 6 50000 100000 50000 100000 1 1000000000" "tests\17" 17 +call scripts\gen-input-via-stdout.bat "files\gen.exe 7 50000 100000 50000 100000 1 1000000000" "tests\18" 18 +call scripts\gen-input-via-stdout.bat "files\gen.exe 8 50000 100000 50000 100000 1 1000000000" "tests\19" 19 +call scripts\gen-input-via-stdout.bat "files\gen.exe 9 50000 100000 50000 100000 1 1000000000" "tests\20" 20 +call scripts\gen-input-via-stdout.bat "files\gen.exe 10 50000 100000 50000 100000 1 1000000000" "tests\21" 21 +call scripts\gen-input-via-stdout.bat "files\gen.exe 1 100000 100000 100000 100000 1 1000000000" "tests\22" 22 +call scripts\gen-input-via-stdout.bat "files\gen.exe 2 100000 100000 100000 100000 1 1000000000" "tests\23" 23 +call scripts\gen-input-via-stdout.bat "files\gen.exe 3 100000 100000 100000 100000 1 1000000000" "tests\24" 24 +call scripts\gen-input-via-stdout.bat "files\gen.exe 4 100000 100000 100000 100000 1 1000000000" "tests\25" 25 +call scripts\gen-input-via-stdout.bat "files\gen.exe 5 100000 100000 100000 100000 1 1000000000" "tests\26" 26 +call scripts\gen-input-via-stdout.bat "files\gen.exe 6 100000 100000 100000 100000 1 1000000000" "tests\27" 27 +call scripts\gen-input-via-stdout.bat "files\gen.exe 7 100000 100000 100000 100000 1 1000000000" "tests\28" 28 +call scripts\gen-input-via-stdout.bat "files\gen.exe 8 100000 100000 100000 100000 1 1000000000" "tests\29" 29 +call scripts\gen-input-via-stdout.bat "files\gen.exe 9 100000 100000 100000 100000 1 1000000000" "tests\30" 30 +call scripts\gen-input-via-stdout.bat "files\gen.exe 10 100000 100000 100000 100000 1 1000000000" "tests\31" 31 +call scripts\gen-input-via-stdout.bat "files\gen1.exe 1 100000 100000 100000 100000 1 1000000000" "tests\32" 32 +call scripts\gen-input-via-stdout.bat "files\gen1.exe 2 100000 100000 100000 100000 1 1000000000" "tests\33" 33 +call scripts\gen-input-via-stdout.bat "files\gen1.exe 3 100000 100000 100000 100000 1 1000000000" "tests\34" 34 +call scripts\gen-input-via-stdout.bat "files\gen1.exe 4 100000 100000 100000 100000 1 1000000000" "tests\35" 35 +call scripts\gen-input-via-stdout.bat "files\gen1.exe 5 100000 100000 100000 100000 1 1000000000" "tests\36" 36 +call scripts\gen-input-via-stdout.bat "files\gen2.exe 1 100000 100000 100000 100000 1 1000000000" "tests\37" 37 +call scripts\gen-input-via-stdout.bat "files\gen2.exe 2 100000 100000 100000 100000 1 1000000000" "tests\38" 38 +call scripts\gen-input-via-stdout.bat "files\gen2.exe 3 100000 100000 100000 100000 1 1000000000" "tests\39" 39 +call scripts\gen-input-via-stdout.bat "files\gen2.exe 4 100000 100000 100000 100000 1 1000000000" "tests\40" 40 +call scripts\gen-input-via-stdout.bat "files\gen2.exe 5 100000 100000 100000 100000 1 1000000000" "tests\41" 41 +call scripts\gen-input-via-stdout.bat "files\print.exe 1" "tests\42" 42 +call scripts\gen-input-via-stdout.bat "files\print.exe 2" "tests\43" 43 +call scripts\gen-input-via-stdout.bat "files\print.exe 3" "tests\44" 44 +call scripts\gen-input-via-stdout.bat "files\print.exe 4" "tests\45" 45 +call scripts\gen-input-via-stdout.bat "files\print.exe 5" "tests\46" 46 +call scripts\gen-input-via-stdout.bat "files\print.exe 6" "tests\47" 47 +call scripts\gen-input-via-stdout.bat "files\print.exe 7" "tests\48" 48 +call scripts\gen-input-via-stdout.bat "files\print.exe 8" "tests\49" 49 +call scripts\gen-input-via-stdout.bat "files\print.exe 9" "tests\50" 50 +call scripts\gen-input-via-stdout.bat "files\print.exe 10" "tests\51" 51 +call scripts\gen-answer.bat tests\01 tests\01.a "tests" "SAMPLES" +call scripts\gen-answer.bat tests\02 tests\02.a "tests" "POINTS" +call scripts\gen-answer.bat tests\03 tests\03.a "tests" "POINTS" +call scripts\gen-answer.bat tests\04 tests\04.a "tests" "POINTS" +call scripts\gen-answer.bat tests\05 tests\05.a "tests" "POINTS" +call scripts\gen-answer.bat tests\06 tests\06.a "tests" "POINTS" +call scripts\gen-answer.bat tests\07 tests\07.a "tests" "POINTS" +call scripts\gen-answer.bat tests\08 tests\08.a "tests" "POINTS" +call scripts\gen-answer.bat tests\09 tests\09.a "tests" "POINTS" +call scripts\gen-answer.bat tests\10 tests\10.a "tests" "POINTS" +call scripts\gen-answer.bat tests\11 tests\11.a "tests" "POINTS" +call scripts\gen-answer.bat tests\12 tests\12.a "tests" "POINTS" +call scripts\gen-answer.bat tests\13 tests\13.a "tests" "POINTS" +call scripts\gen-answer.bat tests\14 tests\14.a "tests" "POINTS" +call scripts\gen-answer.bat tests\15 tests\15.a "tests" "POINTS" +call scripts\gen-answer.bat tests\16 tests\16.a "tests" "POINTS" +call scripts\gen-answer.bat tests\17 tests\17.a "tests" "POINTS" +call scripts\gen-answer.bat tests\18 tests\18.a "tests" "POINTS" +call scripts\gen-answer.bat tests\19 tests\19.a "tests" "POINTS" +call scripts\gen-answer.bat tests\20 tests\20.a "tests" "POINTS" +call scripts\gen-answer.bat tests\21 tests\21.a "tests" "POINTS" +call scripts\gen-answer.bat tests\22 tests\22.a "tests" "POINTS" +call scripts\gen-answer.bat tests\23 tests\23.a "tests" "POINTS" +call scripts\gen-answer.bat tests\24 tests\24.a "tests" "POINTS" +call scripts\gen-answer.bat tests\25 tests\25.a "tests" "POINTS" +call scripts\gen-answer.bat tests\26 tests\26.a "tests" "POINTS" +call scripts\gen-answer.bat tests\27 tests\27.a "tests" "POINTS" +call scripts\gen-answer.bat tests\28 tests\28.a "tests" "POINTS" +call scripts\gen-answer.bat tests\29 tests\29.a "tests" "POINTS" +call scripts\gen-answer.bat tests\30 tests\30.a "tests" "POINTS" +call scripts\gen-answer.bat tests\31 tests\31.a "tests" "POINTS" +call scripts\gen-answer.bat tests\32 tests\32.a "tests" "POINTS" +call scripts\gen-answer.bat tests\33 tests\33.a "tests" "POINTS" +call scripts\gen-answer.bat tests\34 tests\34.a "tests" "POINTS" +call scripts\gen-answer.bat tests\35 tests\35.a "tests" "POINTS" +call scripts\gen-answer.bat tests\36 tests\36.a "tests" "POINTS" +call scripts\gen-answer.bat tests\37 tests\37.a "tests" "POINTS" +call scripts\gen-answer.bat tests\38 tests\38.a "tests" "POINTS" +call scripts\gen-answer.bat tests\39 tests\39.a "tests" "POINTS" +call scripts\gen-answer.bat tests\40 tests\40.a "tests" "POINTS" +call scripts\gen-answer.bat tests\41 tests\41.a "tests" "POINTS" +call scripts\gen-answer.bat tests\42 tests\42.a "tests" "POINTS" +call scripts\gen-answer.bat tests\43 tests\43.a "tests" "POINTS" +call scripts\gen-answer.bat tests\44 tests\44.a "tests" "POINTS" +call scripts\gen-answer.bat tests\45 tests\45.a "tests" "POINTS" +call scripts\gen-answer.bat tests\46 tests\46.a "tests" "POINTS" +call scripts\gen-answer.bat tests\47 tests\47.a "tests" "POINTS" +call scripts\gen-answer.bat tests\48 tests\48.a "tests" "POINTS" +call scripts\gen-answer.bat tests\49 tests\49.a "tests" "POINTS" +call scripts\gen-answer.bat tests\50 tests\50.a "tests" "POINTS" +call scripts\gen-answer.bat tests\51 tests\51.a "tests" "POINTS" + diff --git a/exam-queue-17/doall.sh b/exam-queue-17/doall.sh new file mode 100644 index 0000000..05e31b4 --- /dev/null +++ b/exam-queue-17/doall.sh @@ -0,0 +1,262 @@ +#!/usr/bin/env bash +# *** validation *** +scripts/run-validator-tests.sh +scripts/run-checker-tests.sh + +# *** tests *** +mkdir -p tests +echo "Generating test #2" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 1 5 10 5 10 1 100" "tests/02" 2 +echo "Generating test #3" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 2 5 10 5 10 1 100" "tests/03" 3 +echo "Generating test #4" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 3 5 10 5 10 1 100" "tests/04" 4 +echo "Generating test #5" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 4 5 10 5 10 1 100" "tests/05" 5 +echo "Generating test #6" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 5 5 10 5 10 1 100" "tests/06" 6 +echo "Generating test #7" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 1 50 100 500 1000 1 1000000" "tests/07" 7 +echo "Generating test #8" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 2 50 100 500 1000 1 1000000" "tests/08" 8 +echo "Generating test #9" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 3 50 100 500 1000 1 1000000" "tests/09" 9 +echo "Generating test #10" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 4 50 100 500 1000 1 1000000" "tests/10" 10 +echo "Generating test #11" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 5 50 100 500 1000 1 1000000" "tests/11" 11 +echo "Generating test #12" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 1 50000 100000 50000 100000 1 1000000000" "tests/12" 12 +echo "Generating test #13" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 2 50000 100000 50000 100000 1 1000000000" "tests/13" 13 +echo "Generating test #14" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 3 50000 100000 50000 100000 1 1000000000" "tests/14" 14 +echo "Generating test #15" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 4 50000 100000 50000 100000 1 1000000000" "tests/15" 15 +echo "Generating test #16" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 5 50000 100000 50000 100000 1 1000000000" "tests/16" 16 +echo "Generating test #17" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 6 50000 100000 50000 100000 1 1000000000" "tests/17" 17 +echo "Generating test #18" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 7 50000 100000 50000 100000 1 1000000000" "tests/18" 18 +echo "Generating test #19" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 8 50000 100000 50000 100000 1 1000000000" "tests/19" 19 +echo "Generating test #20" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 9 50000 100000 50000 100000 1 1000000000" "tests/20" 20 +echo "Generating test #21" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 10 50000 100000 50000 100000 1 1000000000" "tests/21" 21 +echo "Generating test #22" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 1 100000 100000 100000 100000 1 1000000000" "tests/22" 22 +echo "Generating test #23" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 2 100000 100000 100000 100000 1 1000000000" "tests/23" 23 +echo "Generating test #24" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 3 100000 100000 100000 100000 1 1000000000" "tests/24" 24 +echo "Generating test #25" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 4 100000 100000 100000 100000 1 1000000000" "tests/25" 25 +echo "Generating test #26" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 5 100000 100000 100000 100000 1 1000000000" "tests/26" 26 +echo "Generating test #27" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 6 100000 100000 100000 100000 1 1000000000" "tests/27" 27 +echo "Generating test #28" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 7 100000 100000 100000 100000 1 1000000000" "tests/28" 28 +echo "Generating test #29" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 8 100000 100000 100000 100000 1 1000000000" "tests/29" 29 +echo "Generating test #30" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 9 100000 100000 100000 100000 1 1000000000" "tests/30" 30 +echo "Generating test #31" +scripts/gen-input-via-stdout.sh "wine files/gen.exe 10 100000 100000 100000 100000 1 1000000000" "tests/31" 31 +echo "Generating test #32" +scripts/gen-input-via-stdout.sh "wine files/gen1.exe 1 100000 100000 100000 100000 1 1000000000" "tests/32" 32 +echo "Generating test #33" +scripts/gen-input-via-stdout.sh "wine files/gen1.exe 2 100000 100000 100000 100000 1 1000000000" "tests/33" 33 +echo "Generating test #34" +scripts/gen-input-via-stdout.sh "wine files/gen1.exe 3 100000 100000 100000 100000 1 1000000000" "tests/34" 34 +echo "Generating test #35" +scripts/gen-input-via-stdout.sh "wine files/gen1.exe 4 100000 100000 100000 100000 1 1000000000" "tests/35" 35 +echo "Generating test #36" +scripts/gen-input-via-stdout.sh "wine files/gen1.exe 5 100000 100000 100000 100000 1 1000000000" "tests/36" 36 +echo "Generating test #37" +scripts/gen-input-via-stdout.sh "wine files/gen2.exe 1 100000 100000 100000 100000 1 1000000000" "tests/37" 37 +echo "Generating test #38" +scripts/gen-input-via-stdout.sh "wine files/gen2.exe 2 100000 100000 100000 100000 1 1000000000" "tests/38" 38 +echo "Generating test #39" +scripts/gen-input-via-stdout.sh "wine files/gen2.exe 3 100000 100000 100000 100000 1 1000000000" "tests/39" 39 +echo "Generating test #40" +scripts/gen-input-via-stdout.sh "wine files/gen2.exe 4 100000 100000 100000 100000 1 1000000000" "tests/40" 40 +echo "Generating test #41" +scripts/gen-input-via-stdout.sh "wine files/gen2.exe 5 100000 100000 100000 100000 1 1000000000" "tests/41" 41 +echo "Generating test #42" +scripts/gen-input-via-stdout.sh "wine files/print.exe 1" "tests/42" 42 +echo "Generating test #43" +scripts/gen-input-via-stdout.sh "wine files/print.exe 2" "tests/43" 43 +echo "Generating test #44" +scripts/gen-input-via-stdout.sh "wine files/print.exe 3" "tests/44" 44 +echo "Generating test #45" +scripts/gen-input-via-stdout.sh "wine files/print.exe 4" "tests/45" 45 +echo "Generating test #46" +scripts/gen-input-via-stdout.sh "wine files/print.exe 5" "tests/46" 46 +echo "Generating test #47" +scripts/gen-input-via-stdout.sh "wine files/print.exe 6" "tests/47" 47 +echo "Generating test #48" +scripts/gen-input-via-stdout.sh "wine files/print.exe 7" "tests/48" 48 +echo "Generating test #49" +scripts/gen-input-via-stdout.sh "wine files/print.exe 8" "tests/49" 49 +echo "Generating test #50" +scripts/gen-input-via-stdout.sh "wine files/print.exe 9" "tests/50" 50 +echo "Generating test #51" +scripts/gen-input-via-stdout.sh "wine files/print.exe 10" "tests/51" 51 +echo "" +echo "Generating answer for test #1" +scripts/gen-answer.sh tests/01 tests/01.a "tests" "SAMPLES" +echo "" +echo "Generating answer for test #2" +scripts/gen-answer.sh tests/02 tests/02.a "tests" "POINTS" +echo "" +echo "Generating answer for test #3" +scripts/gen-answer.sh tests/03 tests/03.a "tests" "POINTS" +echo "" +echo "Generating answer for test #4" +scripts/gen-answer.sh tests/04 tests/04.a "tests" "POINTS" +echo "" +echo "Generating answer for test #5" +scripts/gen-answer.sh tests/05 tests/05.a "tests" "POINTS" +echo "" +echo "Generating answer for test #6" +scripts/gen-answer.sh tests/06 tests/06.a "tests" "POINTS" +echo "" +echo "Generating answer for test #7" +scripts/gen-answer.sh tests/07 tests/07.a "tests" "POINTS" +echo "" +echo "Generating answer for test #8" +scripts/gen-answer.sh tests/08 tests/08.a "tests" "POINTS" +echo "" +echo "Generating answer for test #9" +scripts/gen-answer.sh tests/09 tests/09.a "tests" "POINTS" +echo "" +echo "Generating answer for test #10" +scripts/gen-answer.sh tests/10 tests/10.a "tests" "POINTS" +echo "" +echo "Generating answer for test #11" +scripts/gen-answer.sh tests/11 tests/11.a "tests" "POINTS" +echo "" +echo "Generating answer for test #12" +scripts/gen-answer.sh tests/12 tests/12.a "tests" "POINTS" +echo "" +echo "Generating answer for test #13" +scripts/gen-answer.sh tests/13 tests/13.a "tests" "POINTS" +echo "" +echo "Generating answer for test #14" +scripts/gen-answer.sh tests/14 tests/14.a "tests" "POINTS" +echo "" +echo "Generating answer for test #15" +scripts/gen-answer.sh tests/15 tests/15.a "tests" "POINTS" +echo "" +echo "Generating answer for test #16" +scripts/gen-answer.sh tests/16 tests/16.a "tests" "POINTS" +echo "" +echo "Generating answer for test #17" +scripts/gen-answer.sh tests/17 tests/17.a "tests" "POINTS" +echo "" +echo "Generating answer for test #18" +scripts/gen-answer.sh tests/18 tests/18.a "tests" "POINTS" +echo "" +echo "Generating answer for test #19" +scripts/gen-answer.sh tests/19 tests/19.a "tests" "POINTS" +echo "" +echo "Generating answer for test #20" +scripts/gen-answer.sh tests/20 tests/20.a "tests" "POINTS" +echo "" +echo "Generating answer for test #21" +scripts/gen-answer.sh tests/21 tests/21.a "tests" "POINTS" +echo "" +echo "Generating answer for test #22" +scripts/gen-answer.sh tests/22 tests/22.a "tests" "POINTS" +echo "" +echo "Generating answer for test #23" +scripts/gen-answer.sh tests/23 tests/23.a "tests" "POINTS" +echo "" +echo "Generating answer for test #24" +scripts/gen-answer.sh tests/24 tests/24.a "tests" "POINTS" +echo "" +echo "Generating answer for test #25" +scripts/gen-answer.sh tests/25 tests/25.a "tests" "POINTS" +echo "" +echo "Generating answer for test #26" +scripts/gen-answer.sh tests/26 tests/26.a "tests" "POINTS" +echo "" +echo "Generating answer for test #27" +scripts/gen-answer.sh tests/27 tests/27.a "tests" "POINTS" +echo "" +echo "Generating answer for test #28" +scripts/gen-answer.sh tests/28 tests/28.a "tests" "POINTS" +echo "" +echo "Generating answer for test #29" +scripts/gen-answer.sh tests/29 tests/29.a "tests" "POINTS" +echo "" +echo "Generating answer for test #30" +scripts/gen-answer.sh tests/30 tests/30.a "tests" "POINTS" +echo "" +echo "Generating answer for test #31" +scripts/gen-answer.sh tests/31 tests/31.a "tests" "POINTS" +echo "" +echo "Generating answer for test #32" +scripts/gen-answer.sh tests/32 tests/32.a "tests" "POINTS" +echo "" +echo "Generating answer for test #33" +scripts/gen-answer.sh tests/33 tests/33.a "tests" "POINTS" +echo "" +echo "Generating answer for test #34" +scripts/gen-answer.sh tests/34 tests/34.a "tests" "POINTS" +echo "" +echo "Generating answer for test #35" +scripts/gen-answer.sh tests/35 tests/35.a "tests" "POINTS" +echo "" +echo "Generating answer for test #36" +scripts/gen-answer.sh tests/36 tests/36.a "tests" "POINTS" +echo "" +echo "Generating answer for test #37" +scripts/gen-answer.sh tests/37 tests/37.a "tests" "POINTS" +echo "" +echo "Generating answer for test #38" +scripts/gen-answer.sh tests/38 tests/38.a "tests" "POINTS" +echo "" +echo "Generating answer for test #39" +scripts/gen-answer.sh tests/39 tests/39.a "tests" "POINTS" +echo "" +echo "Generating answer for test #40" +scripts/gen-answer.sh tests/40 tests/40.a "tests" "POINTS" +echo "" +echo "Generating answer for test #41" +scripts/gen-answer.sh tests/41 tests/41.a "tests" "POINTS" +echo "" +echo "Generating answer for test #42" +scripts/gen-answer.sh tests/42 tests/42.a "tests" "POINTS" +echo "" +echo "Generating answer for test #43" +scripts/gen-answer.sh tests/43 tests/43.a "tests" "POINTS" +echo "" +echo "Generating answer for test #44" +scripts/gen-answer.sh tests/44 tests/44.a "tests" "POINTS" +echo "" +echo "Generating answer for test #45" +scripts/gen-answer.sh tests/45 tests/45.a "tests" "POINTS" +echo "" +echo "Generating answer for test #46" +scripts/gen-answer.sh tests/46 tests/46.a "tests" "POINTS" +echo "" +echo "Generating answer for test #47" +scripts/gen-answer.sh tests/47 tests/47.a "tests" "POINTS" +echo "" +echo "Generating answer for test #48" +scripts/gen-answer.sh tests/48 tests/48.a "tests" "POINTS" +echo "" +echo "Generating answer for test #49" +scripts/gen-answer.sh tests/49 tests/49.a "tests" "POINTS" +echo "" +echo "Generating answer for test #50" +scripts/gen-answer.sh tests/50 tests/50.a "tests" "POINTS" +echo "" +echo "Generating answer for test #51" +scripts/gen-answer.sh tests/51 tests/51.a "tests" "POINTS" +echo "" + diff --git a/exam-queue-17/files/check.cpp b/exam-queue-17/files/check.cpp new file mode 100644 index 0000000..327d347 --- /dev/null +++ b/exam-queue-17/files/check.cpp @@ -0,0 +1,57 @@ +#include "testlib.h" +#include + +using namespace std; + +int main(int argc, char * argv[]) +{ + setName("compare ordered sequences of signed int%d numbers", 8 * int(sizeof(long long))); + + registerTestlibCmd(argc, argv); + + int n = 0; + string firstElems; + + while (!ans.seekEof() && !ouf.seekEof()) + { + n++; + long long j = ans.readLong(); + long long p = ouf.readLong(); + if (j != p) + quitf(_wa, "%d%s numbers differ - expected: '%s', found: '%s'", n, englishEnding(n).c_str(), vtos(j).c_str(), vtos(p).c_str()); + else + if (n <= 5) + { + if (firstElems.length() > 0) + firstElems += " "; + firstElems += vtos(j); + } + } + + int extraInAnsCount = 0; + + while (!ans.seekEof()) + { + ans.readLong(); + extraInAnsCount++; + } + + int extraInOufCount = 0; + + while (!ouf.seekEof()) + { + ouf.readLong(); + extraInOufCount++; + } + + if (extraInAnsCount > 0) + quitf(_wa, "Answer contains longer sequence [length = %d], but output contains %d elements", n + extraInAnsCount, n); + + if (extraInOufCount > 0) + quitf(_wa, "Output contains longer sequence [length = %d], but answer contains %d elements", n + extraInOufCount, n); + + if (n <= 5) + quitf(_ok, "%d number(s): \"%s\"", n, compress(firstElems).c_str()); + else + quitf(_ok, "%d numbers", n); +} diff --git a/exam-queue-17/files/gen.cpp b/exam-queue-17/files/gen.cpp new file mode 100644 index 0000000..37bfef6 --- /dev/null +++ b/exam-queue-17/files/gen.cpp @@ -0,0 +1,281 @@ +#include + +#ifndef KRAKOZYABRA +#include "testlib.h" +#else +#include "testliblochal.h" +#endif // KRAKOZYABRA + +using namespace std; + +template +class ordered_set { +private: + struct Node { + T data; + int weight; // Number of nodes in the subtree rooted at this node + std::unique_ptr left; + std::unique_ptr right; + + Node(const T& data) : data(data), weight(1), left(nullptr), right(nullptr) {} + }; + + std::unique_ptr root; + + // Helper function to update the weight of a node + void update_weight(Node* node) { + if (node) { + node->weight = 1; + if (node->left) node->weight += node->left->weight; + if (node->right) node->weight += node->right->weight; + } + } + + // Helper function for insertion + Node* insert_recursive(Node* node, const T& data) { + if (!node) { + return new Node(data); + } + + if (data < node->data) { + node->left.reset(insert_recursive(node->left.release(), data)); //Release ownership before recursive call + } + else { + node->right.reset(insert_recursive(node->right.release(), data)); //Release ownership before recursive call + } + update_weight(node); + return node; + } + + // Helper function for deletion (find minimum in right subtree) + Node* find_min(Node* node) { + while (node->left) { + node = node->left.get(); // Access the raw pointer + } + return node; + } + + // Helper function for deletion + Node* delete_recursive(Node* node, const T& data) { + if (!node) { + return nullptr; // Value not found + } + + if (data < node->data) { + node->left.reset(delete_recursive(node->left.release(), data)); //Release ownership before recursive call + } + else if (data > node->data) { + node->right.reset(delete_recursive(node->right.release(), data)); //Release ownership before recursive call + } + else { + // Node to be deleted found + + // Case 1: Node with no child or only one child + if (!node->left) { + Node* temp = node->right.release(); + delete node; + return temp; + } + else if (!node->right) { + Node* temp = node->left.release(); + delete node; + return temp; + } + + // Case 2: Node with two children + Node* temp = find_min(node->right.get()); + node->data = temp->data; + node->right.reset(delete_recursive(node->right.release(), temp->data)); + } + + update_weight(node); + return node; + } + + //Helper for get_element_at_index (find the k-th smallest element). + T get_element_at_index_recursive(Node* node, int index) { + if (!node) { + throw std::out_of_range("Index out of range"); + } + + int left_subtree_size = (node->left) ? node->left->weight : 0; + + if (index == left_subtree_size) { + return node->data; + } + else if (index < left_subtree_size) { + return get_element_at_index_recursive(node->left.get(), index); + } + else { + return get_element_at_index_recursive(node->right.get(), index - left_subtree_size - 1); + } + } + + //Helper to perform inorder traversal + void inorder_traversal_recursive(Node* node, std::vector& result) const { + if (node) { + inorder_traversal_recursive(node->left.get(), result); + result.push_back(node->data); + inorder_traversal_recursive(node->right.get(), result); + } + } + + //Helper to find index of an element. + int get_index_of_element_recursive(Node* node, const T& target, int current_rank) const { + if (!node) { + return -1; // Element not found + } + + int left_subtree_size = (node->left) ? node->left->weight : 0; + + if (target == node->data) { + return current_rank + left_subtree_size; + } + else if (target < node->data) { + return get_index_of_element_recursive(node->left.get(), target, current_rank); + } + else { + return get_index_of_element_recursive(node->right.get(), target, current_rank + left_subtree_size + 1); + } + } + + +public: + ordered_set() : root(nullptr) {} + + void insert(const T& data) { + if (!root) { + root = std::make_unique(data); + } + else { + root.reset(insert_recursive(root.release(), data)); //Release ownership before recursive call + } + } + + void erase(const T& data) { + root.reset(delete_recursive(root.release(), data)); //Release ownership before recursive call + } + + T get_element(int index) { + if (index < 0 || index >= size()) { + throw std::out_of_range("Index out of range"); + } + return get_element_at_index_recursive(root.get(), index); + } + + int size() const { + return (root) ? root->weight : 0; + } + + bool empty() const { + return root == nullptr; + } + + void clear() { + root.reset(); // Effectively deletes the entire tree + } + + // Returns a vector of the elements in sorted order (inorder traversal) + std::vector inorder_traversal() const { + std::vector result; + inorder_traversal_recursive(root.get(), result); + return result; + } + + // Added function to get the index of an element + int get_index(const T target) const { + return get_index_of_element_recursive(root.get(), target, 0); + } +}; + +int main(int argc, char* argv[]) { +#ifndef KRAKOZYABRA + registerGen(argc, argv, 1); +#endif // KRAKOZYABRA + + + int minN = opt(2), maxN = opt(3); + int minM = opt(4), maxM = opt(5); + int minA = opt(6), maxA = opt(7); + int n = rnd.next(minN, maxN); + int m = rnd.next(minM, maxM); + cout << n << ' ' << m << '\n'; + ordered_set st; + vector a; + while (a.size() < n) { + int aa = rnd.next(minA, maxA); + if (st.get_index(aa) == -1) { + a.push_back(aa); + st.insert(aa); + } + } + + auto getrndin = [&]() { + if (st.size() == 0) + return -1; + int sz = st.size(); + int i = rnd.next(0, sz - 1); + return st.get_element(i); + }; + auto getrndnew = [&]() { + int aa = rnd.next(minA, maxA); + while (st.get_index(aa) != -1) + aa = rnd.next(minA, maxA); + return aa; + }; + + for (int i = 0; i < n; i++) + cout << a[i] << " \n"[i == n - 1]; + for (int i = 0; i < m; i++) { + int t = 0; + if (st.size() == 0) + t = 2; + else + t = rnd.next(1, 3); + if (t == 1) { + int x = getrndnew(); + int y = getrndin(); + st.insert(x); + cout << t << ' ' << x << ' ' << y << '\n'; + } + if (t == 2) { + int x = getrndnew(); + st.insert(x); + cout << t << ' ' << x << '\n'; + } + if (t == 3) { + int x = getrndin(); + st.erase(x); + cout << t << ' ' << x << '\n'; + } + } +} + +//#pragma once +//#include +//#include +//#include +// +//using namespace std; +// +//random_device rd; +//mt19937 gen(rd()); +// +//template +//T opt(T value) { +// FILE* stream; +// freopen_s(&stream, "params.txt", "r", stdin); +// T res; +// for (int i = 0; i < value; i++) +// cin >> res; +// return res; +//} +// +//class Random { +//public: +// long long next(long long a, long long b) { +// return a + gen() % (b - a + 1); +// }; +//}; +// +//Random rnd; \ No newline at end of file diff --git a/exam-queue-17/files/gen.exe b/exam-queue-17/files/gen.exe new file mode 100644 index 0000000..4ec8c7b Binary files /dev/null and b/exam-queue-17/files/gen.exe differ diff --git a/exam-queue-17/files/gen1.cpp b/exam-queue-17/files/gen1.cpp new file mode 100644 index 0000000..7a1e1b2 --- /dev/null +++ b/exam-queue-17/files/gen1.cpp @@ -0,0 +1,280 @@ +#include + +#ifndef KRAKOZYABRA +#include "testlib.h" +#else +#include "testliblochal.h" +#endif // KRAKOZYABRA + +using namespace std; + +template +class ordered_set { +private: + struct Node { + T data; + int weight; // Number of nodes in the subtree rooted at this node + std::unique_ptr left; + std::unique_ptr right; + + Node(const T& data) : data(data), weight(1), left(nullptr), right(nullptr) {} + }; + + std::unique_ptr root; + + // Helper function to update the weight of a node + void update_weight(Node* node) { + if (node) { + node->weight = 1; + if (node->left) node->weight += node->left->weight; + if (node->right) node->weight += node->right->weight; + } + } + + // Helper function for insertion + Node* insert_recursive(Node* node, const T& data) { + if (!node) { + return new Node(data); + } + + if (data < node->data) { + node->left.reset(insert_recursive(node->left.release(), data)); //Release ownership before recursive call + } + else { + node->right.reset(insert_recursive(node->right.release(), data)); //Release ownership before recursive call + } + update_weight(node); + return node; + } + + // Helper function for deletion (find minimum in right subtree) + Node* find_min(Node* node) { + while (node->left) { + node = node->left.get(); // Access the raw pointer + } + return node; + } + + // Helper function for deletion + Node* delete_recursive(Node* node, const T& data) { + if (!node) { + return nullptr; // Value not found + } + + if (data < node->data) { + node->left.reset(delete_recursive(node->left.release(), data)); //Release ownership before recursive call + } + else if (data > node->data) { + node->right.reset(delete_recursive(node->right.release(), data)); //Release ownership before recursive call + } + else { + // Node to be deleted found + + // Case 1: Node with no child or only one child + if (!node->left) { + Node* temp = node->right.release(); + delete node; + return temp; + } + else if (!node->right) { + Node* temp = node->left.release(); + delete node; + return temp; + } + + // Case 2: Node with two children + Node* temp = find_min(node->right.get()); + node->data = temp->data; + node->right.reset(delete_recursive(node->right.release(), temp->data)); + } + + update_weight(node); + return node; + } + + //Helper for get_element_at_index (find the k-th smallest element). + T get_element_at_index_recursive(Node* node, int index) { + if (!node) { + throw std::out_of_range("Index out of range"); + } + + int left_subtree_size = (node->left) ? node->left->weight : 0; + + if (index == left_subtree_size) { + return node->data; + } + else if (index < left_subtree_size) { + return get_element_at_index_recursive(node->left.get(), index); + } + else { + return get_element_at_index_recursive(node->right.get(), index - left_subtree_size - 1); + } + } + + //Helper to perform inorder traversal + void inorder_traversal_recursive(Node* node, std::vector& result) const { + if (node) { + inorder_traversal_recursive(node->left.get(), result); + result.push_back(node->data); + inorder_traversal_recursive(node->right.get(), result); + } + } + + //Helper to find index of an element. + int get_index_of_element_recursive(Node* node, const T& target, int current_rank) const { + if (!node) { + return -1; // Element not found + } + + int left_subtree_size = (node->left) ? node->left->weight : 0; + + if (target == node->data) { + return current_rank + left_subtree_size; + } + else if (target < node->data) { + return get_index_of_element_recursive(node->left.get(), target, current_rank); + } + else { + return get_index_of_element_recursive(node->right.get(), target, current_rank + left_subtree_size + 1); + } + } + + +public: + ordered_set() : root(nullptr) {} + + void insert(const T& data) { + if (!root) { + root = std::make_unique(data); + } + else { + root.reset(insert_recursive(root.release(), data)); //Release ownership before recursive call + } + } + + void erase(const T& data) { + root.reset(delete_recursive(root.release(), data)); //Release ownership before recursive call + } + + T get_element(int index) { + if (index < 0 || index >= size()) { + throw std::out_of_range("Index out of range"); + } + return get_element_at_index_recursive(root.get(), index); + } + + int size() const { + return (root) ? root->weight : 0; + } + + bool empty() const { + return root == nullptr; + } + + void clear() { + root.reset(); // Effectively deletes the entire tree + } + + // Returns a vector of the elements in sorted order (inorder traversal) + std::vector inorder_traversal() const { + std::vector result; + inorder_traversal_recursive(root.get(), result); + return result; + } + + // Added function to get the index of an element + int get_index(const T target) const { + return get_index_of_element_recursive(root.get(), target, 0); + } +}; + +int main(int argc, char* argv[]) { +#ifndef KRAKOZYABRA + registerGen(argc, argv, 1); +#endif // KRAKOZYABRA + + + int minN = opt(2), maxN = opt(3); + int minM = opt(4), maxM = opt(5); + int minA = opt(6), maxA = opt(7); + int n = rnd.next(minN, maxN); + int m = rnd.next(minM, maxM); + cout << n << ' ' << m << '\n'; + ordered_set st; + vector a; + while (a.size() < n) { + int aa = rnd.next(minA, maxA); + if (st.get_index(aa) == -1) { + a.push_back(aa); + st.insert(aa); + } + } + + auto getrndin = [&]() { + if (st.size() == 0) + return -1; + int sz = st.size(); + int i = rnd.next(0, sz - 1); + return st.get_element(i); + }; + auto getrndnew = [&]() { + int aa = rnd.next(minA, maxA); + while (st.get_index(aa) != -1) + aa = rnd.next(minA, maxA); + return aa; + }; + + for (int i = 0; i < n; i++) + cout << a[i] << " \n"[i == n - 1]; + for (int i = 0; i < m; i++) { + int t = 1; + //if (st.size() == 0) + // t = 2; + //else + // t = rnd.next(1, 3); + if (t == 1) { + int x = getrndnew(); + int y = getrndin(); + st.insert(x); + cout << t << ' ' << x << ' ' << y << '\n'; + } + if (t == 2) { + int x = getrndnew(); + st.insert(x); + cout << t << ' ' << x << '\n'; + } + if (t == 3) { + int x = getrndin(); + st.erase(x); + cout << t << ' ' << x << '\n'; + } + } +} + +//#pragma once +//#include +//#include +// +//using namespace std; +// +//random_device rd; +//mt19937 gen(rd()); +// +//template +//T opt(T value) { +// FILE* stream; +// freopen_s(&stream, "params.txt", "r", stdin); +// T res; +// for (int i = 0; i < value; i++) +// cin >> res; +// return res; +//} +// +//class Random { +//public: +// long long next(long long a, long long b) { +// return a + gen() % (b - a + 1); +// }; +//}; +// +//Random rnd; \ No newline at end of file diff --git a/exam-queue-17/files/gen1.exe b/exam-queue-17/files/gen1.exe new file mode 100644 index 0000000..73a2e0b Binary files /dev/null and b/exam-queue-17/files/gen1.exe differ diff --git a/exam-queue-17/files/gen2.cpp b/exam-queue-17/files/gen2.cpp new file mode 100644 index 0000000..9e1aac6 --- /dev/null +++ b/exam-queue-17/files/gen2.cpp @@ -0,0 +1,276 @@ +#include + +#ifndef KRAKOZYABRA +#include "testlib.h" +#else +#include "testliblochal.h" +#endif // KRAKOZYABRA + +using namespace std; + +template +class ordered_set { +private: + struct Node { + T data; + int weight; // Number of nodes in the subtree rooted at this node + std::unique_ptr left; + std::unique_ptr right; + + Node(const T& data) : data(data), weight(1), left(nullptr), right(nullptr) {} + }; + + std::unique_ptr root; + + // Helper function to update the weight of a node + void update_weight(Node* node) { + if (node) { + node->weight = 1; + if (node->left) node->weight += node->left->weight; + if (node->right) node->weight += node->right->weight; + } + } + + // Helper function for insertion + Node* insert_recursive(Node* node, const T& data) { + if (!node) { + return new Node(data); + } + + if (data < node->data) { + node->left.reset(insert_recursive(node->left.release(), data)); //Release ownership before recursive call + } + else { + node->right.reset(insert_recursive(node->right.release(), data)); //Release ownership before recursive call + } + update_weight(node); + return node; + } + + // Helper function for deletion (find minimum in right subtree) + Node* find_min(Node* node) { + while (node->left) { + node = node->left.get(); // Access the raw pointer + } + return node; + } + + // Helper function for deletion + Node* delete_recursive(Node* node, const T& data) { + if (!node) { + return nullptr; // Value not found + } + + if (data < node->data) { + node->left.reset(delete_recursive(node->left.release(), data)); //Release ownership before recursive call + } + else if (data > node->data) { + node->right.reset(delete_recursive(node->right.release(), data)); //Release ownership before recursive call + } + else { + // Node to be deleted found + + // Case 1: Node with no child or only one child + if (!node->left) { + Node* temp = node->right.release(); + delete node; + return temp; + } + else if (!node->right) { + Node* temp = node->left.release(); + delete node; + return temp; + } + + // Case 2: Node with two children + Node* temp = find_min(node->right.get()); + node->data = temp->data; + node->right.reset(delete_recursive(node->right.release(), temp->data)); + } + + update_weight(node); + return node; + } + + //Helper for get_element_at_index (find the k-th smallest element). + T get_element_at_index_recursive(Node* node, int index) { + if (!node) { + throw std::out_of_range("Index out of range"); + } + + int left_subtree_size = (node->left) ? node->left->weight : 0; + + if (index == left_subtree_size) { + return node->data; + } + else if (index < left_subtree_size) { + return get_element_at_index_recursive(node->left.get(), index); + } + else { + return get_element_at_index_recursive(node->right.get(), index - left_subtree_size - 1); + } + } + + //Helper to perform inorder traversal + void inorder_traversal_recursive(Node* node, std::vector& result) const { + if (node) { + inorder_traversal_recursive(node->left.get(), result); + result.push_back(node->data); + inorder_traversal_recursive(node->right.get(), result); + } + } + + //Helper to find index of an element. + int get_index_of_element_recursive(Node* node, const T& target, int current_rank) const { + if (!node) { + return -1; // Element not found + } + + int left_subtree_size = (node->left) ? node->left->weight : 0; + + if (target == node->data) { + return current_rank + left_subtree_size; + } + else if (target < node->data) { + return get_index_of_element_recursive(node->left.get(), target, current_rank); + } + else { + return get_index_of_element_recursive(node->right.get(), target, current_rank + left_subtree_size + 1); + } + } + + +public: + ordered_set() : root(nullptr) {} + + void insert(const T& data) { + if (!root) { + root = std::make_unique(data); + } + else { + root.reset(insert_recursive(root.release(), data)); //Release ownership before recursive call + } + } + + void erase(const T& data) { + root.reset(delete_recursive(root.release(), data)); //Release ownership before recursive call + } + + T get_element(int index) { + if (index < 0 || index >= size()) { + throw std::out_of_range("Index out of range"); + } + return get_element_at_index_recursive(root.get(), index); + } + + int size() const { + return (root) ? root->weight : 0; + } + + bool empty() const { + return root == nullptr; + } + + void clear() { + root.reset(); // Effectively deletes the entire tree + } + + // Returns a vector of the elements in sorted order (inorder traversal) + std::vector inorder_traversal() const { + std::vector result; + inorder_traversal_recursive(root.get(), result); + return result; + } + + // Added function to get the index of an element + int get_index(const T target) const { + return get_index_of_element_recursive(root.get(), target, 0); + } +}; + +int main(int argc, char* argv[]) { +#ifndef KRAKOZYABRA + registerGen(argc, argv, 1); +#endif // KRAKOZYABRA + + + int minN = opt(2), maxN = opt(3); + int minM = opt(4), maxM = opt(5); + int minA = opt(6), maxA = opt(7); + int n = rnd.next(minN, maxN); + int m = rnd.next(minM, maxM); + cout << n << ' ' << m << '\n'; + ordered_set st; + vector a; + while (a.size() < n) { + int aa = rnd.next(minA, maxA); + if (st.get_index(aa) == -1) { + a.push_back(aa); + st.insert(aa); + } + } + + auto getrndin = [&]() { + if (st.size() == 0) + return -1; + int sz = st.size(); + int i = rnd.next(0, sz - 1); + return st.get_element(i); + }; + auto getrndnew = [&]() { + int aa = rnd.next(minA, maxA); + while (st.get_index(aa) != -1) + aa = rnd.next(minA, maxA); + return aa; + }; + + for (int i = 0; i < n; i++) + cout << a[i] << " \n"[i == n - 1]; + for (int i = 0; i < m; i++) { + int t = rnd.next(1, 2); + if (t == 1) { + int x = getrndnew(); + int y = getrndin(); + st.insert(x); + cout << t << ' ' << x << ' ' << y << '\n'; + } + if (t == 2) { + int x = getrndnew(); + st.insert(x); + cout << t << ' ' << x << '\n'; + } + if (t == 3) { + int x = getrndin(); + st.erase(x); + cout << t << ' ' << x << '\n'; + } + } +} + +//#pragma once +//#include +//#include +// +//using namespace std; +// +//random_device rd; +//mt19937 gen(rd()); +// +//template +//T opt(T value) { +// FILE* stream; +// freopen_s(&stream, "params.txt", "r", stdin); +// T res; +// for (int i = 0; i < value; i++) +// cin >> res; +// return res; +//} +// +//class Random { +//public: +// long long next(long long a, long long b) { +// return a + gen() % (b - a + 1); +// }; +//}; +// +//Random rnd; \ No newline at end of file diff --git a/exam-queue-17/files/gen2.exe b/exam-queue-17/files/gen2.exe new file mode 100644 index 0000000..b78eb68 Binary files /dev/null and b/exam-queue-17/files/gen2.exe differ diff --git a/exam-queue-17/files/olymp.sty b/exam-queue-17/files/olymp.sty new file mode 100644 index 0000000..9c37968 --- /dev/null +++ b/exam-queue-17/files/olymp.sty @@ -0,0 +1,788 @@ +% +% Macros for the contest problems +% for MikTeX: use latex.exe +% License: https://github.com/GassaFM/olymp.sty/blob/master/LICENSE +% Authors: https://github.com/GassaFM/olymp.sty/blob/master/AUTHORS +% + +\ProvidesPackage{olymp} + +\usepackage{verbatim} + + +\newif\if@landscape\@landscapefalse +\newif\if@russian\@russianfalse +\newif\if@ukrainian\@ukrainianfalse +\newif\if@arabic\@arabicfalse + +\DeclareOption{landscape}{ + \@landscapetrue +} +\DeclareOption{russian}{ + \@russiantrue +} +\DeclareOption{ukrainian}{ + \@ukrainiantrue +} +\DeclareOption{arabic}{ + \@arabictrue +} +\ProcessOptions\relax + + +% -- Setup margins -- +% +% Tex defines to large margins for our purposes. +% So we redefine this to use paper more efficiently +% + +\newlength{\thelinewidth} + +\if@landscape + \if@twocolumn + \else + \error Landscape is only supported for two column mode + \fi + + \ifcase\@ptsize % 10 pt + \hoffset=-15mm + \voffset=-35mm + \textheight=165mm + \textwidth=272mm + \or % 11 pt + \hoffset=-13mm + \voffset=-36mm + \textheight=166mm + \textwidth=272mm + \or % 12 pt + \hoffset=-12mm + \voffset=-35mm + \textheight=162mm + \textwidth=268mm + \fi +\else + \ifcase\@ptsize % 10 pt + \hoffset=-26.5mm + \voffset=-35mm + \textheight=250mm + \textwidth=175mm + \or % 11 pt + \hoffset=-25mm + \voffset=-37.5mm + \textheight=255mm + \textwidth=175mm + \or % 12 pt + \hoffset=-20mm + \voffset=-35mm + \textheight=245mm + \textwidth=175mm + \fi + + \if@twocolumn + \hoffset=-14.3mm + \voffset=-38mm + \textheight=255mm + \textwidth=188mm + \fi +\fi + +\if@twocolumn + \thelinewidth=0.47\textwidth +\else + \thelinewidth=\textwidth +\fi + +% -- End of setup margins -- + +%---------- From package "lastpage" ------------------ +\def\lastpage@putlabel{\addtocounter{page}{-1}% + \immediate\write\@auxout{\string\newlabel{LastPage}{{}{\thepage}}}% + \addtocounter{page}{1}} +\AtEndDocument{\clearpage\lastpage@putlabel}% +%---------- end of "lastpage" ------------------ + +% -- Setup sizes -- +\newlength{\exmpwidinf} +\newlength{\exmpwidouf} +\newlength{\exmpwidewid} +\newlength{\exmpthreewidinf} +\newlength{\exmpthreewidouf} +\newlength{\exmpthreewidnote} + +\newif\ifintentionallyblankpages + +\exmpwidinf=0.43\thelinewidth +\exmpwidouf=0.43\thelinewidth +\exmpwidewid=0.9\thelinewidth +\exmpthreewidinf=0.28\thelinewidth +\exmpthreewidouf=0.28\thelinewidth +\exmpthreewidnote=0.30\thelinewidth + +\newlength{\afterproblemhead} +\newlength{\afterconstraints} + +\renewcommand{\t}[1]{\ifmmode{\mathtt{#1}}\else{\texttt{#1}}\fi} +\if@russian +\renewcommand{\le}{\leqslant} +\renewcommand{\ge}{\geqslant} +\renewcommand{\leq}{\leqslant} +\renewcommand{\geq}{\geqslant} +\else +\if@ukrainian +\renewcommand{\le}{\leqslant} +\renewcommand{\ge}{\geqslant} +\renewcommand{\leq}{\leqslant} +\renewcommand{\geq}{\geqslant} +\else +\parindent=0mm +\parskip=1ex +\fi +\fi + +% -- End of setup sizes -- + +% -- Setup keywords -- + +\if@russian +\def\kw@ProblemTutorial{Разбор задачи} +\def\kw@Problem{Задача} +\def\kw@ProblemAuthor{Автор:} +\def\kw@ProblemDeveloper{Разработчик:} +\def\kw@ProblemOrigin{Источник:} +\def\kw@InputFileName{Имя входного файла:} +\def\kw@OutputFileName{Имя выходного файла:} +\def\kw@TimeLimit{Ограничение по времени:} +\def\kw@MemoryLimit{Ограничение по памяти:} +\def\kw@Feedback{Отображение результатов:} +\def\kw@stdin{стандартный поток ввода} +\def\kw@stdout{стандартный поток вывода} +\def\kw@Specification{Спецификация} +\def\kw@Interaction{Протокол взаимодействия} +\def\kw@Input{Формат входных данных} +\def\kw@Output{Формат выходных данных} +\def\kw@Example{Пример} +\def\kw@Examples{Примеры} +\def\kwExampleNotes{Пояснение} +\def\kw@Explanation{Пояснение к примеру} +\def\kw@Explanations{Пояснения к примерам} +\def\kw@Illustration{Иллюстрация} +\def\kw@Scoring{Система оценки} +\def\kw@Note{Замечание} +\def\kw@Notes{Замечания} +\def\kw@Constraints{Ограничения} +\def\kw@version{версия} +\def\kw@revision{ревизия} +\def\kw@SubtaskOne{Подзадача 1} +\def\kw@SubtaskTwo{Подзадача 2} +\def\kw@SubtaskThree{Подзадача 3} +\def\kw@SubtaskFour{Подзадача 4} +\def\kw@SubtaskFive{Подзадача 5} +\def\kw@SubtaskSix{Подзадача 6} +\def\kw@Subtask{Подзадача} +\def\kw@points{баллы} +\def\kw@Page{Страница} +\def\kw@of{из} +\def\kw@notstated{не указан} +\def\kw@IntentionallyBlankPage{Эта страница специально оставлена пустой} +\def\kw@defaultinputname{тест} +\def\kw@defaultoutputname{ответ} +\else +\if@ukrainian +\def\kw@ProblemTutorial{Розбір задачі} +\def\kw@Problem{Задача} +\def\kw@ProblemAuthor{Автор:} +\def\kw@ProblemDeveloper{Розробник:} +\def\kw@ProblemOrigin{Джерело:} +\def\kw@InputFileName{Назва вхідного файлу:} +\def\kw@OutputFileName{Назва вихідного файлу:} +\def\kw@TimeLimit{Ліміт часу:} +\def\kw@MemoryLimit{Ліміт використання пам'яті:} +\def\kw@Feedback{Відображення результатів:} +\def\kw@stdin{стандартний потік вводу} +\def\kw@stdout{стандартний потік виводу} +\def\kw@Specification{Специфікація} +\def\kw@Interaction{Протокол взаємодії} +\def\kw@Input{Формат вхідних даних} +\def\kw@Output{Формат вихідних даних} +\def\kw@Example{Приклад} +\def\kw@Examples{Приклади} +\def\kwExampleNotes{Пояснення} +\def\kw@Explanation{Пояснення до прикладу} +\def\kw@Explanations{Пояснения до прикладів} +\def\kw@Illustration{Ілюстрація} +\def\kw@Scoring{Система оцінювання} +\def\kw@Note{Зауваження} +\def\kw@Notes{Зауваження} +\def\kw@Constraints{Обмеження} +\def\kw@version{версія} +\def\kw@revision{ревізія} +\def\kw@SubtaskOne{Підзадача 1} +\def\kw@SubtaskTwo{Підзадача 2} +\def\kw@SubtaskThree{Підзадача 3} +\def\kw@SubtaskFour{Підзадача 4} +\def\kw@SubtaskFive{Підзадача 5} +\def\kw@SubtaskSix{Підзадача 6} +\def\kw@Subtask{Підзадача} +\def\kw@points{бали} +\def\kw@Page{Сторінка} +\def\kw@of{з} +\def\kw@notstated{не вказано} +\def\kw@IntentionallyBlankPage{Ця сторінка спеціально залишена порожньою} +\def\kw@defaultinputname{тест} +\def\kw@defaultoutputname{відповідь} +\else +\def\kw@ProblemTutorial{Problem Tutorial} +\def\kw@Problem{Problem} +\def\kw@ProblemAuthor{Author:} +\def\kw@ProblemDeveloper{Developer:} +\def\kw@ProblemOrigin{Origin:} +\def\kw@InputFileName{Input file:} +\def\kw@OutputFileName{Output file:} +\def\kw@TimeLimit{Time limit:} +\def\kw@MemoryLimit{Memory limit:} +\def\kw@Feedback{Feedback:} +\def\kw@stdin{standard input} +\def\kw@stdout{standard output} +\def\kw@Specification{Specification} +\def\kw@Interaction{Interaction Protocol} +\def\kw@Input{Input} +\def\kw@Output{Output} +\def\kw@Example{Example} +\def\kw@Examples{Examples} +\def\kwExampleNotes{Notes} +\def\kw@Explanation{Explanation} +\def\kw@Explanations{Explanations} +\def\kw@Illustration{Illustration} +\def\kw@Scoring{Scoring} +\def\kw@Note{Note} +\def\kw@Notes{Notes} +\def\kw@Constraints{Constraints} +\def\kw@version{version} +\def\kw@revision{revision} +\def\kw@SubtaskOne{Subtask 1} +\def\kw@SubtaskTwo{Subtask 2} +\def\kw@SubtaskThree{Subtask 3} +\def\kw@SubtaskFour{Subtask 4} +\def\kw@SubtaskFive{Subtask 5} +\def\kw@SubtaskSix{Subtask 6} +\def\kw@Subtask{Subtask} +\def\kw@points{points} +\def\kw@Page{Page} +\def\kw@of{of} +\def\kw@notstated{not stated} +\def\kw@IntentionallyBlankPage{This page is intentionally left blank} +\def\kw@defaultinputname{test} +\def\kw@defaultoutputname{answer} +\fi +\fi + +\afterproblemhead=3mm +\afterconstraints=2mm + +\newcommand{\problemheadfont}{\LARGE} +\newcommand{\problemsectionfont}{\Large} +\newcommand{\problemend}{ +\clearpage +\ifintentionallyblankpages + \ifodd\value{page} + \else + \vspace*{\fill} + \begin{center} + \problemheadfont\kw@IntentionallyBlankPage + \end{center} + \vspace*{\fill} + \clearpage + \fi +\fi +} +\newcommand{\problemtextfont}{\normalsize} +\newcommand{\beforeproblemsectioncaption}{\smallbreak\smallskip} +\newcommand{\afterproblemsectioncaption}{\smallskip} + +\if@twocolumn + \afterproblemhead=1mm + \afterconstraints=1mm + \renewcommand{\problemheadfont}{\large} + \renewcommand{\problemsectionfont}{\normalsize} + \renewcommand{\problemend}{\par\medskip} + \renewcommand{\problemtextfont}{\footnotesize} + \renewcommand{\beforeproblemsectioncaption}{\smallbreak\smallskip} + \renewcommand{\afterproblemsectioncaption}{} +\fi + +% -- End of setup keywords -- + + +% -- Problem sections -- + +\newcommand{\createsection}{\@newsection} + +\def\@newsection#1#2{\DeclareRobustCommand{#1}{ +{\beforeproblemsectioncaption\noindent\bf\problemsectionfont +\textsf{#2}} +\nopagebreak\par\afterproblemsectioncaption} +} + +\newcommand{\createsectionexample}{\@newsectionexample} + +\def\@newsectionexample#1#2{\DeclareRobustCommand{#1}{ +\ifdefined\NoExamples\else% +{\beforeproblemsectioncaption\noindent\bf\problemsectionfont +\textsf{#2}} +\nopagebreak\par\afterproblemsectioncaption% +\fi% +} +} + +\newcommand{\createsectionpar}{\@newsectionpar} + +\def\@newsectionpar#1#2{\DeclareRobustCommand{#1}[1]{ +{\beforeproblemsectioncaption\noindent\bf\problemsectionfont +\textsf{#2~##1}} +\nopagebreak\par\afterproblemsectioncaption} +} + +\newcommand{\createsectionpartwo}{\@newsectionpartwo} + +\def\@newsectionpartwo#1#2#3{\DeclareRobustCommand{#1}[2]{ +{\beforeproblemsectioncaption\noindent\problemsectionfont +\textsf{\textbf{#2}~\textbf{##1}~(##2~#3)}} +\nopagebreak\par\afterproblemsectioncaption} +} + +\createsection{\Specification}{\kw@Specification} +\createsection{\Interaction}{\kw@Interaction} +\createsection{\InputFile}{\kw@Input} +\createsection{\OutputFile}{\kw@Output} +\createsectionexample{\Example}{\kw@Example} +\createsectionexample{\Examples}{\kw@Examples} +\createsection{\Explanation}{\kw@Explanation} +\createsection{\Explanations}{\kw@Explanations} +\createsection{\Illustration}{\kw@Illustration} +\createsection{\Scoring}{\kw@Scoring} +\createsection{\Note}{\kw@Note} +\createsection{\Notes}{\kw@Notes} +\createsection{\Constraints}{\kw@Constraints} +\createsection{\SubtaskOne}{\kw@SubtaskOne} +\createsection{\SubtaskTwo}{\kw@SubtaskTwo} +\createsection{\SubtaskThree}{\kw@SubtaskThree} +\createsection{\SubtaskFour}{\kw@SubtaskFour} +\createsection{\SubtaskFive}{\kw@SubtaskFive} +\createsection{\SubtaskSix}{\kw@SubtaskSix} +\createsectionpar{\Subtask}{\kw@Subtask} +\createsectionpartwo{\SubtaskWithCost}{\kw@Subtask}{\kw@points} + +% -- End of problem sections + +% -- Default limits definition -- + +\if@russian +\def\defaulttimelimit{2 секунды} +\else +\if@ukrainian +\def\defaulttimelimit{2 секунди} +\else +\def\defaulttimelimit{2 seconds} +\fi +\fi + + + +\if@russian +\def\defaultmemorylimit{256 мебибайт} +\else +\if@ukrainian +\def\defaulttimelimit{256 мебібайт} +\else +\def\defaultmemorylimit{256 mebibytes} +\fi +\fi + +% -- End of default limits definition -- + +% -- Problem environment -- + +\def\defaultproblemauthor{\textit{\kw@notstated}} +\gdef\thisproblemauthor{\defaultproblemauthor} +\def\defaultproblemdeveloper{\textit{\kw@notstated}} +\gdef\thisproblemdeveloper{\defaultproblemdeveloper} +\def\defaultproblemorigin{\textit{\kw@notstated}} +\gdef\thisproblemorigin{\defaultproblemorigin} + +\newif\ifdisplayauthor +\newif\ifdisplaydeveloper +\newif\ifdisplayorigin +\newif\ifrevisionsignature +\newif\ifdisplayauthorinfooter +\newif\ifdisplaydeveloperinfooter + +\newcounter{problem} +\newcounter{subtasknum}[problem] + +\newcommand{\SubtaskWithScore}[1]{% +{\addtocounter{subtasknum}{1}% +\beforeproblemsectioncaption\noindent\problemsectionfont% +\textsf{\textbf{\kw@Subtask~\arabic{subtasknum}}}% +\textsf{~(\kw@points:~#1)}% +\nopagebreak\par\afterproblemsectioncaption}% +}% + +\newenvironment{tutorial}[1]{% + \bigskip% + \noindent% + \refstepcounter{problem} + \textbf{\problemheadfont\textsf{\ifdefined\ShortProblemTitle\ifdefined\ProblemIndex\ProblemIndex. \fi\else\kw@Problem\ \ifdefined\ProblemIndex\ProblemIndex\else\if@arabic\arabic{problem}\else\Alph{problem}\fi\fi. \fi #1}}% + \nopagebreak% + \problemtextfont% +} + +\newenvironment{@problem}[6]{ +\global\let\lastproblemauthor\thisproblemauthor +\global\let\lastproblemdeveloper\thisproblemdeveloper +\global\let\lastproblemorigin\thisproblemorigin + +% Hotfix +%\def\@memorylimit{#5} + +%% -- Default memory limit -- +%% :FIXME: +%\def\@t{#5} +% +%\ifx\@t\empty +% \def\@memorylimit{\defaultmemorylimit} +%\else +%%\ifcat\par\@t +%% \def\@memorylimit{\defaultmemorylimit} +%%\else +% \def\@memorylimit{#5} +%%\fi +%\fi +%% -- End of default memory limit -- + +% -- No feedback case -- +% :FIXME: +\def\@t{#6} + +\ifx\@t\empty + \def\@feedback{} +\else +%\ifcat\par\@t +% \def\@feedback{} +%\else + \def\@feedback{\kw@Feedback & #6 \\} +\fi +%\fi +% -- End of no feedback case -- + + { + \ifdefined\NoProblemHead\else% + \noindent + \refstepcounter{problem} + \textbf{\problemheadfont\textsf{% + \ifdefined\ShortProblemTitle\ifdefined\ProblemIndex\ProblemIndex. \fi\else\kw@Problem\ \ifdefined\ProblemIndex\ProblemIndex\else\if@arabic\arabic{problem}\else\Alph{problem}\fi\fi. \fi% + #1% + \ifdefined\DivisionNumber% + \if\DivisionNumber2% + {\ \textit{(Division\ \DivisionNumber)}}% + \fi% + \fi% + }}% + \nopagebreak% + \par\vspace{\afterproblemhead}% + \problemtextfont\parindent=6.5mm% + \vbox{ + \begin{tabular}{l@{\extracolsep{1cm}}l} + \ifdisplayauthor% + \kw@ProblemAuthor & \thisproblemauthor \\ + \fi% + \ifdisplaydeveloper% + \kw@ProblemDeveloper & \thisproblemdeveloper \\ + \fi% + \ifdisplayorigin% + \kw@ProblemOrigin & \thisproblemorigin \\ + \fi% + \ifdefined\NoInputFileName\else% + \ifx&% + \else% + \kw@InputFileName & \texttt{#2} \\ + \fi\fi% + \ifdefined\NoOutputFileName\else% + \ifx&% + \else% + \kw@OutputFileName & \texttt{#3} \\ + \fi\fi% + \ifdefined\NoTimeLimit\else% + \ifx&% + \else% + \kw@TimeLimit & #4 \\ + \fi\fi% + \ifdefined\NoMemoryLimit\else% + \ifx&% + \else% + \kw@MemoryLimit & #5 \\ + \fi\fi% +% \kw@MemoryLimit & \@memorylimit \\ + \@feedback + \end{tabular} + } + \nopagebreak + \par\vspace{\afterconstraints} + \fi% + } + \problemtextfont + + \newcommand{\InputFileName}{\ifx\relax#2\relax{\kw@defaultinputname}\else{#2}\fi} + \newcommand{\OutputFileName}{\ifx\relax#3\relax{\kw@defaultoutputname}\else{#3}\fi} +}{% + \global\let\lastproblemauthor\thisproblemauthor% + \global\let\lastproblemdeveloper\thisproblemdeveloper% + \global\let\lastproblemdorigin\thisproblemorigin% + \gdef\thisproblemauthor{\defaultproblemauthor}% + \gdef\thisproblemdeveloper{\defaultproblemdeveloper}% + \gdef\thisproblemorigin{\defaultproblemorigin}% + \problemend% +} + +\def\s@tm@cr@s{ + \def\widthin##1{\exmpwidinf=##1\relax} + \def\widthout##1{\exmpwidouf=##1\relax} + \def\stretchin##1{\advance\exmpwidinf by ##1\relax} + \def\stretchout##1{\advance\exmpwidouf by ##1\relax} + \@ifstar{ + \error Star must not be used in example environment any more + } +} + +% This is magic, which delete space after verbatiminput +\addto@hook{\every@verbatim}{\topsep=0pt\relax} + +% :FIXME: +\newenvironment{example}[1][]{ + \s@tm@cr@s#1 + \ttfamily\obeylines\obeyspaces\frenchspacing + \newcommand{\exmp}[2]{ + \ifdefined\NoExamples\else% + \begin{minipage}[t]{\exmpwidinf}\rightskip=0pt plus 1fill\relax##1\medskip\end{minipage}& + \begin{minipage}[t]{\exmpwidouf}\rightskip=0pt plus 1fill\relax##2\medskip\end{minipage}\\ + \hline + \fi% + } + + \newcommand{\exmpfile}[2]{ + \ifdefined\NoExamples\else% + \exmp{ + \verbatiminput{##1} + }{ + \verbatiminput{##2} + }% + \fi% + } + + + \ifdefined\NoExamples\else% + \begin{tabular}{|l|l|} + \hline + \multicolumn{1}{|c|}{\bf\texttt{\InputFileName}}& + \multicolumn{1}{c|}{\bf\texttt{\OutputFileName}}\\ + \hline + \fi% +}{ + \ifdefined\NoExamples\else% + \end{tabular} + \fi% +} + +\newenvironment{examplewide}[1][]{% + \s@tm@cr@s#1 + \ttfamily\obeylines\obeyspaces\frenchspacing + \newcommand{\exmp}[2]{ + \ifdefined\NoExamples\else% + \begin{tabular}{|c|} + \hline + \multicolumn{1}{|c|}{\bf\texttt{\InputFileName}}\\ + \hline + \begin{minipage}[t]{\exmpwidewid}\rightskip=0pt plus 1fill\relax + ##1 + \medskip\end{minipage}\\ + \hline + \multicolumn{1}{|c|}{\bf\texttt{\OutputFileName}}\\% + \hline + \begin{minipage}[t]{\exmpwidewid}\rightskip=0pt plus 1fill\relax + ##2 + \medskip\end{minipage}\\% + \hline + \end{tabular} + \fi% + } + \newcommand{\exmpfile}[2]{ + \ifdefined\NoExamples\else% + \exmp{ + \verbatiminput{##1} + }{ + \verbatiminput{##2} + }% + \fi% + } +}{ +} + +\newenvironment{examplethree}[1][]{ + \s@tm@cr@s#1 + \ttfamily\obeylines\obeyspaces\frenchspacing + \newcommand{\exmp}[3]{ + \ifdefined\NoExamples\else% + \begin{minipage}[t]{\exmpthreewidinf}\rightskip=0pt plus 1fill\relax##1\medskip\end{minipage}& + \begin{minipage}[t]{\exmpthreewidouf}\rightskip=0pt plus 1fill\relax##2\medskip\end{minipage}& + \begin{minipage}[t]{\exmpthreewidnote}\rightskip=0pt plus 1fill\relax##3\medskip\end{minipage}\\ + \hline + \fi% + } + + \newcommand{\exmpfile}[3]{ + \ifdefined\NoExamples\else% + \exmp{ + \verbatiminput{##1} + }{ + \verbatiminput{##2} + }{ + ##3 + }% + \fi% + } + + + \ifdefined\NoExamples\else% + \begin{tabular}{|l|l|l|} + \hline + \multicolumn{1}{|c|}{\bf\texttt{\InputFileName}}& + \multicolumn{1}{|c|}{\bf\texttt{\OutputFileName}}& + \multicolumn{1}{|c|}{\bf\texttt{\expandafter\unexpanded{\expandafter\kwExampleNotes}}}\\ + \hline + \fi% +}{ + \ifdefined\NoExamples\else% + \end{tabular} + \fi% +} + +% -- This is hack to make feedback argument optional + +\newenvironment{problem}[5]{% +\def\@ProblemCommon{\begin{@problem}{#1}{#2}{#3}{#4}{#5}}% +\newcommand\@problemSix[1]{\@ProblemCommon{##1}}% +\newcommand\@problemFive{\@ProblemCommon{}}% +\@ifnextchar\bgroup\@problemSix\@problemFive% +}{% +\end{@problem}% +} + + +% -- End of problem environment -- + + +% -- Declare "shortitems" and "shortnums" environment: it's a "compact itemize" -- +\if@twocolumn + \def\shortitems{\vspace{-1mmplus6mm}\itemize\itemsep-0.618mmplus0.5mm\relax}% + \def\endshortitems{\vspace{-1mmplus6mm}\enditemize}% + \def\shortnums{\vspace{-1mmplus6mm}\enumerate\itemsep-0.618mmplus0.5mm\relax}% + \def\endshortnums{\vspace{-1mmplus6mm}\endenumerate}% +\else + \def\shortitems{\vspace{-3mmplus2mm}\itemize\itemsep-1.618mmplus0.5mm\relax}% + \def\endshortitems{\vspace{-3mmplus2mm}\enditemize}% + \def\shortnums{\vspace{-3mmplus2mm}\enumerate\itemsep-1.618mmplus0.5mm\relax}% + \def\endshortnums{\vspace{-3mmplus2mm}\endenumerate}% +\fi +% -- end of "shortitems" and "shortnums" declaration -- + +\newcommand{\thecontestname}{Olympiad in Informatics} +\newcommand{\thecontestlocation}{Somewhere} +\newcommand{\thecontestdate}{Once upon a time} +\newcommand{\therevision}{undefined} + +\DeclareRobustCommand{\contestname}{\thecontestname\par\thecontestlocation\unskip, \thecontestdate} + +\DeclareRobustCommand{\contest}[3]{ + \renewcommand{\thecontestname}{#1} + \renewcommand{\thecontestlocation}{#2} + \renewcommand{\thecontestdate}{#3} + + \def\temp{#1}\ifx\temp\empty + \def\temp{#2}\ifx\temp\empty + \def\temp{#3}\ifx\temp\empty + \let\thecontestname\undefined% + \let\thecontestlocation\undefined% + \let\thecontestdate\undefined% + \fi + \fi + \fi +} + +\DeclareRobustCommand{\revision}[1]{ + \renewcommand{\therevision}{#1} +} + +\makeatletter + +\renewcommand{\@oddhead}{ + \ifdefined\thecontestname + \parbox{\textwidth}{ + \sffamily + \begin{center} + \protect\contestname + \\[2pt] + \hrule + \end{center} + } + \fi +} + +\renewcommand{\@oddfoot}{ +\gdef\problemletter{\if@arabic\arabic{problem}\else\Alph{problem}\fi} + +% Revision signature +\ifrevisionsignature% +%\if@revsign% + {\gdef\rsigfooter{, \kw@revision\ \therevision}}% +\else% + {\gdef\rsigfooter{}}% +\fi% + +\ifdisplayauthorinfooter% +%\if@newfooter% + {\gdef\thefooter% + {~\quad{\kw@Problem\ \problemletter% + \ifdefined\DivisionNumber{ (Div.~\DivisionNumber)}\fi}% + {\hfill}% + {\kw@ProblemAuthor~\lastproblemauthor}% + {\hfill}% + {\kw@Page\ \thepage\ \kw@of\ \pageref{LastPage}\rsigfooter}\quad~}}% +\else% +\ifdisplaydeveloperinfooter% + {\gdef\thefooter% + {~\quad{\kw@Problem\ \problemletter}% + {\hfill}% + {\kw@ProblemDeveloper~\lastproblemdeveloper}% + {\hfill}% + {\kw@Page\ \thepage\ \kw@of\ \pageref{LastPage}\rsigfooter}\quad~}}% +\else% + {\gdef\thefooter% + {{\hfil}\kw@Page\ \thepage\ \kw@of\ \pageref{LastPage}\rsigfooter\hfil}}% +\fi% +\fi% + + \parbox{\textwidth}{ + \hrule + \vspace{6pt} + \sffamily + \thefooter + } +} + +\makeatother + +\headheight=2cm +\headsep=6mm + +\hfuzz=0.5pt + +\sloppy diff --git a/exam-queue-17/files/print.cpp b/exam-queue-17/files/print.cpp new file mode 100644 index 0000000..3af123b --- /dev/null +++ b/exam-queue-17/files/print.cpp @@ -0,0 +1,86 @@ +#define _CRT_SECURE_NO_WARNINGS +#include +#include "testlib.h" + +using namespace std; + +int main(int argc, char* argv[]) { + registerGen(argc, argv, 1); + int t = opt(1); + vector a; + vector> b; + + if (t == 1) { + a.push_back(1); + b.push_back({ 1, 2, 1 }); + } + if (t == 2) { + a.push_back(1); + for (int i = 0; i < 100000; i += 2) { + b.push_back({ 1, 2, 1 }); + b.push_back({ 3, 2 }); + } + } + if (t == 3) { + a.push_back(1e9); + for (int i = 0; i < 50000; i++) + b.push_back({ 2, i + 1 }); + for (int i = 0; i < 50000; i++) + b.push_back({ 3, i + 1 }); + } + if (t == 4) { + for (int i = 0; i < 100000; i++) + a.push_back(i + 1); + for (int i = 0; i < 100000; i++) + b.push_back({ 3, i + 1 }); + } + if (t == 5) { + for (int i = 0; i < 100000; i++) + a.push_back(i + 1); + for (int i = 0; i < 100000; i++) + b.push_back({ 1, 100000 + i + 1, i + 1 }); + } + if (t == 6) { + for (int i = 0; i < 100000; i++) + a.push_back(1e9 - i); + for (int i = 0; i < 50000; i++) { + b.push_back({ 1, i + 1, (int)1e9 - i }); + b.push_back({ 3, (int)1e9 - i }); + } + } + if (t == 7) { + for (int i = 0; i < 100000; i++) + a.push_back(1e9 - i); + for (int i = 0; i < 100000; i++) + b.push_back({ 3, (int)1e9 - i }); + } + if (t == 8) { + for (int i = 0; i < 100000 && b.size() < 100000; i++) + a.push_back(i + 1); + for (int i = 0; i < 100000 && b.size() < 100000; i++) + b.push_back({ 3, (int)1e5 - i }); + } + if (t == 9) { + for (int i = 0; i < 100000; i++) + a.push_back(i + 1); + for (int i = 0; i < 100000 && b.size() < 100000; i += 2) + b.push_back({ 3, i + 1 }); + for (int i = 0; i < 100000 && b.size() < 100000; i += 2) + b.push_back({ 2, i + 1 }); + } + if (t == 10) { + for (int i = 100000; i > 0; i--) + a.push_back(i); + for (int i = 0; i < 100000 && b.size() < 100000; i += 2) + b.push_back({ 3, i + 1 }); + for (int i = 0; i < 100000 && b.size() < 100000; i += 2) + b.push_back({ 2, i + 1 }); + } + cout << a.size() << ' ' << b.size() << '\n'; + for (int i = 0; i < a.size(); i++) + cout << a[i] << " \n"[i + 1 == a.size()]; + for (auto i : b) { + for (int j = 0; j < i.size(); j++) + cout << i[j] << " \n"[j + 1 == i.size()]; + } +} diff --git a/exam-queue-17/files/print.exe b/exam-queue-17/files/print.exe new file mode 100644 index 0000000..8da331c Binary files /dev/null and b/exam-queue-17/files/print.exe differ diff --git a/exam-queue-17/files/problem.tex b/exam-queue-17/files/problem.tex new file mode 100644 index 0000000..9426722 --- /dev/null +++ b/exam-queue-17/files/problem.tex @@ -0,0 +1,79 @@ +\begin{problem}{${problem.name}}<#-- +-->{<#if "stdin" == problem.inputFile><#-- + --><#if "russian" == language>стандартный ввод<#-- + --><#else>standard input<#-- + --><#else>${problem.inputFile}}<#-- +-->{<#if "stdout" == problem.outputFile><#-- + --><#if "russian" == language>стандартный вывод<#-- + --><#else>standard output<#-- + --><#else>${problem.outputFile}}<#-- +--><#assign timeLimit=problem.timeLimit/1000/><#-- +--><#if language="russian"><#-- + --><#if problem.timeLimit%1000!=0||(10<=timeLimit%100&&timeLimit%100<20)||timeLimit%10=0||5<=timeLimit><#-- + -->{${timeLimit?c} секунд}<#-- + --><#else><#-- + --><#if timeLimit%10=1><#-- + -->{${timeLimit?c} секунда}<#-- + --><#else><#-- + -->{${timeLimit?c} секунды}<#-- + --><#-- + --><#-- +--><#else><#-- + -->{${timeLimit?c} second<#if (timeLimit!=1)>s}<#-- +--><#-- +--><#assign memoryLimit=problem.memoryLimit/1048576/><#-- +--><#if language="russian"><#-- + --><#if problem.memoryLimit%1048576==0&&!(10<=memoryLimit%100&&memoryLimit%100<20)&&2<=memoryLimit%10&&memoryLimit%10<5><#-- + -->{${memoryLimit?c} мегабайта} + <#else><#-- + -->{${memoryLimit?c} мегабайт} + +<#else><#-- + -->{${memoryLimit?c} megabyte<#if (memoryLimit>1)>s} + + +<#if providedStatementsCommands?? && providedStatementsCommands?size != 0><#-- + --><#list providedStatementsCommands as command><#-- + -->${command?string} + + + +${problem.legend} + +<#if problem.input?? && (problem.input?length>0)> +\InputFile +${problem.input} + + +<#if problem.output?? && (problem.output?length>0)> +\OutputFile +${problem.output} + + +<#if problem.interaction?? && (problem.interaction?length>0)> +\Interaction +${problem.interaction} + + +<#if problem.scoring?? && (problem.scoring?length>0)> +\Scoring +${problem.scoring} + + +<#if (problem.sampleTests?size>0)> +\Example<#if (problem.sampleTests?size>1)>s + +\begin{example} +<#list problem.sampleTests as test> +\exmpfile{${test.inputFile}}{${test.outputFile}}% + +\end{example} + + +<#if (problem.notes??) && (problem.notes?length > 0)> +\Note +${problem.notes} + + +\end{problem} + diff --git a/exam-queue-17/files/statements.ftl b/exam-queue-17/files/statements.ftl new file mode 100644 index 0000000..abe93fd --- /dev/null +++ b/exam-queue-17/files/statements.ftl @@ -0,0 +1,53 @@ +\documentclass [11pt, a4paper, oneside] {article} + +\usepackage [T2A] {fontenc} +\usepackage [utf8] {inputenc} +\usepackage [english, russian] {babel} +\usepackage {amsmath} +\usepackage {amssymb} +\usepackage <#if contest.language?? && contest.language="russian">[russian]<#elseif contest.language?? && contest.language="ukrainian">[ukrainian]{olymp} +\usepackage {comment} +\usepackage {epigraph} +\usepackage {expdlist} +\usepackage {graphicx} +\usepackage {multirow} +\usepackage {siunitx} +\usepackage {ulem} +%\usepackage {hyperref} +\usepackage {import} +\usepackage {ifpdf} +\usepackage {xparse} +\ifpdf + \DeclareGraphicsRule{*}{mps}{*}{} +\fi + +\begin {document} + +\contest +{${contest.name!}}% +{${contest.location!}}% +{${contest.date!}}% + +\binoppenalty=10000 +\relpenalty=10000 + +\renewcommand{\t}{\texttt} +\renewcommand{\thefootnote}{\fnsymbol{footnote}} + +<#if shortProblemTitle?? && shortProblemTitle> + \def\ShortProblemTitle{} + + +<#list statements as statement> +<#if statement.path??> +\graphicspath{{${statement.path}}} +<#if statement.index??> + \def\ProblemIndex{${statement.index}} + +\import{${statement.path}}{./${statement.file}} +<#else> +\input ${statement.file} + + + +\end {document} diff --git a/exam-queue-17/files/testlib.h b/exam-queue-17/files/testlib.h new file mode 100644 index 0000000..5a3ab43 --- /dev/null +++ b/exam-queue-17/files/testlib.h @@ -0,0 +1,6203 @@ +/* + * It is strictly recommended to include "testlib.h" before any other include + * in your code. In this case testlib overrides compiler specific "random()". + * + * If you can't compile your code and compiler outputs something about + * ambiguous call of "random_shuffle", "rand" or "srand" it means that + * you shouldn't use them. Use "shuffle", and "rnd.next()" instead of them + * because these calls produce stable result for any C++ compiler. Read + * sample generator sources for clarification. + * + * Please read the documentation for class "random_t" and use "rnd" instance in + * generators. Probably, these sample calls will be useful for you: + * rnd.next(); rnd.next(100); rnd.next(1, 2); + * rnd.next(3.14); rnd.next("[a-z]{1,100}"). + * + * Also read about wnext() to generate off-center random distribution. + * + * See https://github.com/MikeMirzayanov/testlib/ to get latest version or bug tracker. + */ + +#ifndef _TESTLIB_H_ +#define _TESTLIB_H_ + +/* + * Copyright (c) 2005-2024 + */ + +#define VERSION "0.9.42-SNAPSHOT" + +/* + * Mike Mirzayanov + * + * This material is provided "as is", with absolutely no warranty expressed + * or implied. Any use is at your own risk. + * + * Permission to use or copy this software for any purpose is hereby granted + * without fee, provided the above notices are retained on all copies. + * Permission to modify the code and to distribute modified code is granted, + * provided the above notices are retained, and a notice that the code was + * modified is included with the above copyright notice. + * + */ + +/* NOTE: This file contains testlib library for C++. + * + * Check, using testlib running format: + * check.exe [ [-appes]], + * If result file is specified it will contain results. + * + * Validator, using testlib running format: + * validator.exe < input.txt, + * It will return non-zero exit code and writes message to standard output. + * + * Generator, using testlib running format: + * gen.exe [parameter-1] [parameter-2] [... paramerter-n] + * You can write generated test(s) into standard output or into the file(s). + * + * Interactor, using testlib running format: + * interactor.exe [ [ [-appes]]], + * Reads test from inf (mapped to args[1]), writes result to tout (mapped to argv[2], + * can be judged by checker later), reads program output from ouf (mapped to stdin), + * writes output to program via stdout (use cout, printf, etc). + */ + +const char *latestFeatures[] = { + "Added ConstantBoundsLog, VariablesLog to validator testOverviewLogFile", + "Use setAppesModeEncoding to change xml encoding from windows-1251 to other", + "rnd.any/wany use distance/advance instead of -/+: now they support sets/multisets", + "Use syntax `int t = inf.readInt(1, 3, \"~t\");` to skip the lower bound check. Tildes can be used on either side or both: ~t, t~, ~t~", + "Supported EJUDGE support in registerTestlibCmd", + "Supported '--testMarkupFileName fn' and '--testCase tc/--testCaseFileName fn' for validators", + "Added opt defaults via opt(key/index, default_val); check unused opts when using has_opt or default opt (turn off this check with suppressEnsureNoUnusedOpt()).", + "For checker added --group and --testset command line params (like for validator), use checker.group() or checker.testset() to get values", + "Added quitpi(points_info, message) function to return with _points exit code 7 and given points_info", + "rnd.partition(size, sum[, min_part=1]) returns random (unsorted) partition which is a representation of the given `sum` as a sum of `size` positive integers (or >=min_part if specified)", + "rnd.distinct(size, n) and rnd.distinct(size, from, to)", + "opt(\"some_missing_key\") returns false now", + "has_opt(key)", + "Abort validator on validator.testset()/validator.group() if registered without using command line", + "Print integer range violations in a human readable way like `violates the range [1, 10^9]`", + "Opts supported: use them like n = opt(\"n\"), in a command line you can use an exponential notation", + "Reformatted", + "Use setTestCase(i) or unsetTestCase() to support test cases (you can use it in any type of program: generator, interactor, validator or checker)", + "Fixed issue #87: readStrictDouble accepts \"-0.00\"", + "Fixed issue #83: added InStream::quitif(condition, ...)", + "Fixed issue #79: fixed missed guard against repeated header include", + "Fixed issue #80: fixed UB in case of huge quitf message", + "Fixed issue #84: added readXs(size, indexBase = 1)", + "Fixed stringstream repeated usage issue", + "Fixed compilation in g++ (for std=c++03)", + "Batch of println functions (support collections, iterator ranges)", + "Introduced rnd.perm(size, first = 0) to generate a `first`-indexed permutation", + "Allow any whitespace in readInts-like functions for non-validators", + "Ignore 4+ command line arguments ifdef EJUDGE", + "Speed up of vtos", + "Show line number in validators in case of incorrect format", + "Truncate huge checker/validator/interactor message", + "Fixed issue with readTokenTo of very long tokens, now aborts with _pe/_fail depending of a stream type", + "Introduced InStream::ensure/ensuref checking a condition, returns wa/fail depending of a stream type", + "Fixed compilation in VS 2015+", + "Introduced space-separated read functions: readWords/readTokens, multilines read functions: readStrings/readLines", + "Introduced space-separated read functions: readInts/readIntegers/readLongs/readUnsignedLongs/readDoubles/readReals/readStrictDoubles/readStrictReals", + "Introduced split/tokenize functions to separate string by given char", + "Introduced InStream::readUnsignedLong and InStream::readLong with unsigned long long parameters", + "Supported --testOverviewLogFileName for validator: bounds hits + features", + "Fixed UB (sequence points) in random_t", + "POINTS_EXIT_CODE returned back to 7 (instead of 0)", + "Removed disable buffers for interactive problems, because it works unexpectedly in wine", + "InStream over string: constructor of InStream from base InStream to inherit policies and std::string", + "Added expectedButFound quit function, examples: expectedButFound(_wa, 10, 20), expectedButFound(_fail, ja, pa, \"[n=%d,m=%d]\", n, m)", + "Fixed incorrect interval parsing in patterns", + "Use registerGen(argc, argv, 1) to develop new generator, use registerGen(argc, argv, 0) to compile old generators (originally created for testlib under 0.8.7)", + "Introduced disableFinalizeGuard() to switch off finalization checkings", + "Use join() functions to format a range of items as a single string (separated by spaces or other separators)", + "Use -DENABLE_UNEXPECTED_EOF to enable special exit code (by default, 8) in case of unexpected eof. It is good idea to use it in interactors", + "Use -DUSE_RND_AS_BEFORE_087 to compile in compatibility mode with random behavior of versions before 0.8.7", + "Fixed bug with nan in stringToDouble", + "Fixed issue around overloads for size_t on x64", + "Added attribute 'points' to the XML output in case of result=_points", + "Exit codes can be customized via macros, e.g. -DPE_EXIT_CODE=14", + "Introduced InStream function readWordTo/readTokenTo/readStringTo/readLineTo for faster reading", + "Introduced global functions: format(), englishEnding(), upperCase(), lowerCase(), compress()", + "Manual buffer in InStreams, some IO speed improvements", + "Introduced quitif(bool, const char* pattern, ...) which delegates to quitf() in case of first argument is true", + "Introduced guard against missed quitf() in checker or readEof() in validators", + "Supported readStrictReal/readStrictDouble - to use in validators to check strictly float numbers", + "Supported registerInteraction(argc, argv)", + "Print checker message to the stderr instead of stdout", + "Supported TResult _points to output calculated score, use quitp(...) functions", + "Fixed to be compilable on Mac", + "PC_BASE_EXIT_CODE=50 in case of defined TESTSYS", + "Fixed issues 19-21, added __attribute__ format printf", + "Some bug fixes", + "ouf.readInt(1, 100) and similar calls return WA", + "Modified random_t to avoid integer overflow", + "Truncated checker output [patch by Stepan Gatilov]", + "Renamed class random -> class random_t", + "Supported name parameter for read-and-validation methods, like readInt(1, 2, \"n\")", + "Fixed bug in readDouble()", + "Improved ensuref(), fixed nextLine to work in case of EOF, added startTest()", + "Supported \"partially correct\", example: quitf(_pc(13), \"result=%d\", result)", + "Added shuffle(begin, end), use it instead of random_shuffle(begin, end)", + "Added readLine(const string& ptrn), fixed the logic of readLine() in the validation mode", + "Package extended with samples of generators and validators", + "Written the documentation for classes and public methods in testlib.h", + "Implemented random routine to support generators, use registerGen() to switch it on", + "Implemented strict mode to validate tests, use registerValidation() to switch it on", + "Now ncmp.cpp and wcmp.cpp are return WA if answer is suffix or prefix of the output", + "Added InStream::readLong() and removed InStream::readLongint()", + "Now no footer added to each report by default (use directive FOOTER to switch on)", + "Now every checker has a name, use setName(const char* format, ...) to set it", + "Now it is compatible with TTS (by Kittens Computing)", + "Added \'ensure(condition, message = \"\")\' feature, it works like assert()", + "Fixed compatibility with MS C++ 7.1", + "Added footer with exit code information", + "Added compatibility with EJUDGE (compile with EJUDGE directive)", + "Added compatibility with Contester (compile with CONTESTER directive)" +}; + +#ifdef _MSC_VER +#define _CRT_SECURE_NO_DEPRECATE +#define _CRT_SECURE_NO_WARNINGS +#define _CRT_NO_VA_START_VALIDATION +#endif + +/* Overrides random() for Borland C++. */ +#define random __random_deprecated +#include +#include +#include +#include +#undef random + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef TESTLIB_THROW_EXIT_EXCEPTION_INSTEAD_OF_EXIT +# include +#endif + +#if (_WIN32 || __WIN32__ || __WIN32 || _WIN64 || __WIN64__ || __WIN64 || WINNT || __WINNT || __WINNT__ || __CYGWIN__) +# if !defined(_MSC_VER) || _MSC_VER > 1400 +# define NOMINMAX 1 +# include +# else +# define WORD unsigned short +# include +# endif +# include +# define ON_WINDOWS +# if defined(_MSC_VER) && _MSC_VER > 1400 +# pragma warning( disable : 4127 ) +# pragma warning( disable : 4146 ) +# pragma warning( disable : 4458 ) +# endif +#else +# define WORD unsigned short +# include +#endif + +#if defined(FOR_WINDOWS) && defined(FOR_LINUX) +#error Only one target system is allowed +#endif + +#ifndef LLONG_MIN +#define LLONG_MIN (-9223372036854775807LL - 1) +#endif + +#ifndef ULLONG_MAX +#define ULLONG_MAX (18446744073709551615) +#endif + +#define LF ((char)10) +#define CR ((char)13) +#define TAB ((char)9) +#define SPACE ((char)' ') +#define EOFC (255) + +#ifndef OK_EXIT_CODE +# ifdef CONTESTER +# define OK_EXIT_CODE 0xAC +# else +# define OK_EXIT_CODE 0 +# endif +#endif + +#ifndef WA_EXIT_CODE +# ifdef EJUDGE +# define WA_EXIT_CODE 5 +# elif defined(CONTESTER) +# define WA_EXIT_CODE 0xAB +# else +# define WA_EXIT_CODE 1 +# endif +#endif + +#ifndef PE_EXIT_CODE +# ifdef EJUDGE +# define PE_EXIT_CODE 4 +# elif defined(CONTESTER) +# define PE_EXIT_CODE 0xAA +# else +# define PE_EXIT_CODE 2 +# endif +#endif + +#ifndef FAIL_EXIT_CODE +# ifdef EJUDGE +# define FAIL_EXIT_CODE 6 +# elif defined(CONTESTER) +# define FAIL_EXIT_CODE 0xA3 +# else +# define FAIL_EXIT_CODE 3 +# endif +#endif + +#ifndef DIRT_EXIT_CODE +# ifdef EJUDGE +# define DIRT_EXIT_CODE 6 +# else +# define DIRT_EXIT_CODE 4 +# endif +#endif + +#ifndef POINTS_EXIT_CODE +# define POINTS_EXIT_CODE 7 +#endif + +#ifndef UNEXPECTED_EOF_EXIT_CODE +# define UNEXPECTED_EOF_EXIT_CODE 8 +#endif + +#ifndef PC_BASE_EXIT_CODE +# ifdef TESTSYS +# define PC_BASE_EXIT_CODE 50 +# else +# define PC_BASE_EXIT_CODE 0 +# endif +#endif + +#ifdef __GNUC__ +# define __TESTLIB_STATIC_ASSERT(condition) typedef void* __testlib_static_assert_type[(condition) ? 1 : -1] __attribute__((unused)) +#else +# define __TESTLIB_STATIC_ASSERT(condition) typedef void* __testlib_static_assert_type[(condition) ? 1 : -1] +#endif + +#ifdef ON_WINDOWS +#define I64 "%I64d" +#define U64 "%I64u" +#else +#define I64 "%lld" +#define U64 "%llu" +#endif + +#ifdef _MSC_VER +# define NORETURN __declspec(noreturn) +#elif defined __GNUC__ +# define NORETURN __attribute__ ((noreturn)) +#else +# define NORETURN +#endif + +static char __testlib_format_buffer[16777216]; +static int __testlib_format_buffer_usage_count = 0; + +#define FMT_TO_RESULT(fmt, cstr, result) std::string result; \ + if (__testlib_format_buffer_usage_count != 0) \ + __testlib_fail("FMT_TO_RESULT::__testlib_format_buffer_usage_count != 0"); \ + __testlib_format_buffer_usage_count++; \ + va_list ap; \ + va_start(ap, fmt); \ + vsnprintf(__testlib_format_buffer, sizeof(__testlib_format_buffer), cstr, ap); \ + va_end(ap); \ + __testlib_format_buffer[sizeof(__testlib_format_buffer) - 1] = 0; \ + result = std::string(__testlib_format_buffer); \ + __testlib_format_buffer_usage_count--; \ + +const long long __TESTLIB_LONGLONG_MAX = 9223372036854775807LL; +const int __TESTLIB_MAX_TEST_CASE = 1073741823; + +int __testlib_exitCode; + +bool __testlib_hasTestCase; +int __testlib_testCase = -1; + +void setTestCase(int testCase); + +void unsetTestCase() { + __testlib_hasTestCase = false; + __testlib_testCase = -1; +} + +NORETURN static void __testlib_fail(const std::string &message); + +template +#ifdef __GNUC__ +__attribute__((const)) +#endif +static inline T __testlib_abs(const T &x) { + return x > 0 ? x : -x; +} + +template +#ifdef __GNUC__ +__attribute__((const)) +#endif +static inline T __testlib_min(const T &a, const T &b) { + return a < b ? a : b; +} + +template +#ifdef __GNUC__ +__attribute__((const)) +#endif +static inline T __testlib_max(const T &a, const T &b) { + return a > b ? a : b; +} + +template +#ifdef __GNUC__ +__attribute__((const)) +#endif +static inline T __testlib_crop(T value, T a, T b) { + return __testlib_min(__testlib_max(value, a), --b); +} + +#ifdef __GNUC__ +__attribute__((const)) +#endif +static inline double __testlib_crop(double value, double a, double b) { + value = __testlib_min(__testlib_max(value, a), b); + if (value >= b) + value = std::nexttoward(b, a); + return value; +} + +static bool __testlib_prelimIsNaN(double r) { + volatile double ra = r; +#ifndef __BORLANDC__ + return ((ra != ra) == true) && ((ra == ra) == false) && ((1.0 > ra) == false) && ((1.0 < ra) == false); +#else + return std::_isnan(ra); +#endif +} + +#ifdef __GNUC__ +__attribute__((const)) +#endif +static std::string removeDoubleTrailingZeroes(std::string value) { + while (!value.empty() && value[value.length() - 1] == '0' && value.find('.') != std::string::npos) + value = value.substr(0, value.length() - 1); + if (!value.empty() && value[value.length() - 1] == '.') + return value + '0'; + else + return value; +} + +#ifdef __GNUC__ +__attribute__((const)) +#endif +inline std::string upperCase(std::string s) { + for (size_t i = 0; i < s.length(); i++) + if ('a' <= s[i] && s[i] <= 'z') + s[i] = char(s[i] - 'a' + 'A'); + return s; +} + +#ifdef __GNUC__ +__attribute__((const)) +#endif +inline std::string lowerCase(std::string s) { + for (size_t i = 0; i < s.length(); i++) + if ('A' <= s[i] && s[i] <= 'Z') + s[i] = char(s[i] - 'A' + 'a'); + return s; +} + +#ifdef __GNUC__ +__attribute__ ((format (printf, 1, 2))) +#endif +std::string format(const char *fmt, ...) { + FMT_TO_RESULT(fmt, fmt, result); + return result; +} + +std::string format(const std::string fmt, ...) { + FMT_TO_RESULT(fmt, fmt.c_str(), result); + return result; +} + +#ifdef __GNUC__ +__attribute__((const)) +#endif +static std::string __testlib_part(const std::string &s); + +static bool __testlib_isNaN(double r) { + __TESTLIB_STATIC_ASSERT(sizeof(double) == sizeof(long long)); + volatile double ra = r; + long long llr1, llr2; + std::memcpy((void *) &llr1, (void *) &ra, sizeof(double)); + ra = -ra; + std::memcpy((void *) &llr2, (void *) &ra, sizeof(double)); + long long llnan = 0xFFF8000000000000LL; + return __testlib_prelimIsNaN(r) || llnan == llr1 || llnan == llr2; +} + +static double __testlib_nan() { + __TESTLIB_STATIC_ASSERT(sizeof(double) == sizeof(long long)); +#ifndef NAN + long long llnan = 0xFFF8000000000000LL; + double nan; + std::memcpy(&nan, &llnan, sizeof(double)); + return nan; +#else + return NAN; +#endif +} + +static bool __testlib_isInfinite(double r) { + volatile double ra = r; + return (ra > 1E300 || ra < -1E300); +} + +#ifdef __GNUC__ +__attribute__((const)) +#endif +inline bool doubleCompare(double expected, double result, double MAX_DOUBLE_ERROR) { + MAX_DOUBLE_ERROR += 1E-15; + if (__testlib_isNaN(expected)) { + return __testlib_isNaN(result); + } else if (__testlib_isInfinite(expected)) { + if (expected > 0) { + return result > 0 && __testlib_isInfinite(result); + } else { + return result < 0 && __testlib_isInfinite(result); + } + } else if (__testlib_isNaN(result) || __testlib_isInfinite(result)) { + return false; + } else if (__testlib_abs(result - expected) <= MAX_DOUBLE_ERROR) { + return true; + } else { + double minv = __testlib_min(expected * (1.0 - MAX_DOUBLE_ERROR), + expected * (1.0 + MAX_DOUBLE_ERROR)); + double maxv = __testlib_max(expected * (1.0 - MAX_DOUBLE_ERROR), + expected * (1.0 + MAX_DOUBLE_ERROR)); + return result >= minv && result <= maxv; + } +} + +#ifdef __GNUC__ +__attribute__((const)) +#endif +inline double doubleDelta(double expected, double result) { + double absolute = __testlib_abs(result - expected); + + if (__testlib_abs(expected) > 1E-9) { + double relative = __testlib_abs(absolute / expected); + return __testlib_min(absolute, relative); + } else + return absolute; +} + +/** It does nothing on non-windows and files differ from stdin/stdout/stderr. */ +static void __testlib_set_binary(std::FILE *file) { + if (NULL != file) { +#ifdef ON_WINDOWS +# ifdef _O_BINARY + if (stdin == file) +# ifdef STDIN_FILENO + return void(_setmode(STDIN_FILENO, _O_BINARY)); +# else + return void(_setmode(_fileno(stdin), _O_BINARY)); +# endif + if (stdout == file) +# ifdef STDOUT_FILENO + return void(_setmode(STDOUT_FILENO, _O_BINARY)); +# else + return void(_setmode(_fileno(stdout), _O_BINARY)); +# endif + if (stderr == file) +# ifdef STDERR_FILENO + return void(_setmode(STDERR_FILENO, _O_BINARY)); +# else + return void(_setmode(_fileno(stderr), _O_BINARY)); +# endif +# elif O_BINARY + if (stdin == file) +# ifdef STDIN_FILENO + return void(setmode(STDIN_FILENO, O_BINARY)); +# else + return void(setmode(fileno(stdin), O_BINARY)); +# endif + if (stdout == file) +# ifdef STDOUT_FILENO + return void(setmode(STDOUT_FILENO, O_BINARY)); +# else + return void(setmode(fileno(stdout), O_BINARY)); +# endif + if (stderr == file) +# ifdef STDERR_FILENO + return void(setmode(STDERR_FILENO, O_BINARY)); +# else + return void(setmode(fileno(stderr), O_BINARY)); +# endif +# endif +#endif + } +} + +#if __cplusplus > 199711L || defined(_MSC_VER) +template +#ifdef __GNUC__ +__attribute__((const)) +#endif +static std::string vtos(const T &t, std::true_type) { + if (t == 0) + return "0"; + else { + T n(t); + bool negative = n < 0; + std::string s; + while (n != 0) { + T digit = n % 10; + if (digit < 0) + digit = -digit; + s += char('0' + digit); + n /= 10; + } + std::reverse(s.begin(), s.end()); + return negative ? "-" + s : s; + } +} + +template +static std::string vtos(const T &t, std::false_type) { + std::string s; + static std::stringstream ss; + ss.str(std::string()); + ss.clear(); + ss << t; + ss >> s; + return s; +} + +template +static std::string vtos(const T &t) { + return vtos(t, std::is_integral()); +} + +/* signed case. */ +template +static std::string toHumanReadableString(const T &n, std::false_type) { + if (n == 0) + return vtos(n); + int trailingZeroCount = 0; + T n_ = n; + while (n_ % 10 == 0) + n_ /= 10, trailingZeroCount++; + if (trailingZeroCount >= 7) { + if (n_ == 1) + return "10^" + vtos(trailingZeroCount); + else if (n_ == -1) + return "-10^" + vtos(trailingZeroCount); + else + return vtos(n_) + "*10^" + vtos(trailingZeroCount); + } else + return vtos(n); +} + +/* unsigned case. */ +template +static std::string toHumanReadableString(const T &n, std::true_type) { + if (n == 0) + return vtos(n); + int trailingZeroCount = 0; + T n_ = n; + while (n_ % 10 == 0) + n_ /= 10, trailingZeroCount++; + if (trailingZeroCount >= 7) { + if (n_ == 1) + return "10^" + vtos(trailingZeroCount); + else + return vtos(n_) + "*10^" + vtos(trailingZeroCount); + } else + return vtos(n); +} + +template +static std::string toHumanReadableString(const T &n) { + return toHumanReadableString(n, std::is_unsigned()); +} +#else +template +static std::string vtos(const T& t) +{ + std::string s; + static std::stringstream ss; + ss.str(std::string()); + ss.clear(); + ss << t; + ss >> s; + return s; +} + +template +static std::string toHumanReadableString(const T &n) { + return vtos(n); +} +#endif + +template +static std::string toString(const T &t) { + return vtos(t); +} + +#if __cplusplus > 199711L || defined(_MSC_VER) +/* opts */ +void prepareOpts(int argc, char* argv[]); +#endif + +/* + * Very simple regex-like pattern. + * It used for two purposes: validation and generation. + * + * For example, pattern("[a-z]{1,5}").next(rnd) will return + * random string from lowercase latin letters with length + * from 1 to 5. It is easier to call rnd.next("[a-z]{1,5}") + * for the same effect. + * + * Another samples: + * "mike|john" will generate (match) "mike" or "john"; + * "-?[1-9][0-9]{0,3}" will generate (match) non-zero integers from -9999 to 9999; + * "id-([ac]|b{2})" will generate (match) "id-a", "id-bb", "id-c"; + * "[^0-9]*" will match sequences (empty or non-empty) without digits, you can't + * use it for generations. + * + * You can't use pattern for generation if it contains meta-symbol '*'. Also it + * is not recommended to use it for char-sets with meta-symbol '^' like [^a-z]. + * + * For matching very simple greedy algorithm is used. For example, pattern + * "[0-9]?1" will not match "1", because of greedy nature of matching. + * Alternations (meta-symbols "|") are processed with brute-force algorithm, so + * do not use many alternations in one expression. + * + * If you want to use one expression many times it is better to compile it into + * a single pattern like "pattern p("[a-z]+")". Later you can use + * "p.matches(std::string s)" or "p.next(random_t& rd)" to check matching or generate + * new string by pattern. + * + * Simpler way to read token and check it for pattern matching is "inf.readToken("[a-z]+")". + * + * All spaces are ignored in regex, unless escaped with \. For example, ouf.readLine("NO SOLUTION") + * will expect "NOSOLUTION", the correct call should be ouf.readLine("NO\\ SOLUTION") or + * ouf.readLine(R"(NO\ SOLUTION)") if you prefer raw string literals from C++11. + */ +class random_t; + +class pattern { +public: + /* Create pattern instance by string. */ + pattern(std::string s); + + /* Generate new string by pattern and given random_t. */ + std::string next(random_t &rnd) const; + + /* Checks if given string match the pattern. */ + bool matches(const std::string &s) const; + + /* Returns source string of the pattern. */ + std::string src() const; + +private: + bool matches(const std::string &s, size_t pos) const; + + std::string s; + std::vector children; + std::vector chars; + int from; + int to; +}; + +/* + * Use random_t instances to generate random values. It is preferred + * way to use randoms instead of rand() function or self-written + * randoms. + * + * Testlib defines global variable "rnd" of random_t class. + * Use registerGen(argc, argv, 1) to setup random_t seed be command + * line (to use latest random generator version). + * + * Random generates uniformly distributed values if another strategy is + * not specified explicitly. + */ +class random_t { +private: + unsigned long long seed; + static const unsigned long long multiplier; + static const unsigned long long addend; + static const unsigned long long mask; + static const int lim; + + long long nextBits(int bits) { + if (bits <= 48) { + seed = (seed * multiplier + addend) & mask; + return (long long) (seed >> (48 - bits)); + } else { + if (bits > 63) + __testlib_fail("random_t::nextBits(int bits): n must be less than 64"); + + int lowerBitCount = (random_t::version == 0 ? 31 : 32); + + long long left = (nextBits(31) << 32); + long long right = nextBits(lowerBitCount); + + return left ^ right; + } + } + +public: + static int version; + + /* New random_t with fixed seed. */ + random_t() + : seed(3905348978240129619LL) { + } + + /* Sets seed by command line. */ + void setSeed(int argc, char *argv[]) { + random_t p; + + seed = 3905348978240129619LL; + for (int i = 1; i < argc; i++) { + std::size_t le = std::strlen(argv[i]); + for (std::size_t j = 0; j < le; j++) + seed = seed * multiplier + (unsigned int) (argv[i][j]) + addend; + seed += multiplier / addend; + } + + seed = seed & mask; + } + + /* Sets seed by given value. */ + void setSeed(long long _seed) { + seed = (unsigned long long) _seed; + seed = (seed ^ multiplier) & mask; + } + +#ifndef __BORLANDC__ + + /* Random string value by given pattern (see pattern documentation). */ + std::string next(const std::string &ptrn) { + pattern p(ptrn); + return p.next(*this); + } + +#else + /* Random string value by given pattern (see pattern documentation). */ + std::string next(std::string ptrn) + { + pattern p(ptrn); + return p.next(*this); + } +#endif + + /* Random value in range [0, n-1]. */ + int next(int n) { + if (n <= 0) + __testlib_fail("random_t::next(int n): n must be positive"); + + if ((n & -n) == n) // n is a power of 2 + return (int) ((n * (long long) nextBits(31)) >> 31); + + const long long limit = INT_MAX / n * n; + + long long bits; + do { + bits = nextBits(31); + } while (bits >= limit); + + return int(bits % n); + } + + /* Random value in range [0, n-1]. */ + unsigned int next(unsigned int n) { + if (n >= INT_MAX) + __testlib_fail("random_t::next(unsigned int n): n must be less INT_MAX"); + return (unsigned int) next(int(n)); + } + + /* Random value in range [0, n-1]. */ + long long next(long long n) { + if (n <= 0) + __testlib_fail("random_t::next(long long n): n must be positive"); + + const long long limit = __TESTLIB_LONGLONG_MAX / n * n; + + long long bits; + do { + bits = nextBits(63); + } while (bits >= limit); + + return bits % n; + } + + /* Random value in range [0, n-1]. */ + unsigned long long next(unsigned long long n) { + if (n >= (unsigned long long) (__TESTLIB_LONGLONG_MAX)) + __testlib_fail("random_t::next(unsigned long long n): n must be less LONGLONG_MAX"); + return (unsigned long long) next((long long) (n)); + } + + /* Random value in range [0, n-1]. */ + long next(long n) { + return (long) next((long long) (n)); + } + + /* Random value in range [0, n-1]. */ + unsigned long next(unsigned long n) { + if (n >= (unsigned long) (LONG_MAX)) + __testlib_fail("random_t::next(unsigned long n): n must be less LONG_MAX"); + return (unsigned long) next((unsigned long long) (n)); + } + + /* Returns random value in range [from,to]. */ + int next(int from, int to) { + return int(next((long long) to - from + 1) + from); + } + + /* Returns random value in range [from,to]. */ + unsigned int next(unsigned int from, unsigned int to) { + return (unsigned int) (next((long long) to - from + 1) + from); + } + + /* Returns random value in range [from,to]. */ + long long next(long long from, long long to) { + return next(to - from + 1) + from; + } + + /* Returns random value in range [from,to]. */ + unsigned long long next(unsigned long long from, unsigned long long to) { + if (from > to) + __testlib_fail("random_t::next(unsigned long long from, unsigned long long to): from can't not exceed to"); + return next(to - from + 1) + from; + } + + /* Returns random value in range [from,to]. */ + long next(long from, long to) { + return next(to - from + 1) + from; + } + + /* Returns random value in range [from,to]. */ + unsigned long next(unsigned long from, unsigned long to) { + if (from > to) + __testlib_fail("random_t::next(unsigned long from, unsigned long to): from can't not exceed to"); + return next(to - from + 1) + from; + } + + /* Random double value in range [0, 1). */ + double next() { + long long left = ((long long) (nextBits(26)) << 27); + long long right = nextBits(27); + return __testlib_crop((double) (left + right) / (double) (1LL << 53), 0.0, 1.0); + } + + /* Random double value in range [0, n). */ + double next(double n) { + if (n <= 0.0) + __testlib_fail("random_t::next(double): n should be positive"); + return __testlib_crop(n * next(), 0.0, n); + } + + /* Random double value in range [from, to). */ + double next(double from, double to) { + if (from >= to) + __testlib_fail("random_t::next(double from, double to): from should be strictly less than to"); + return next(to - from) + from; + } + + /* Returns random element from container. */ + template + typename Container::value_type any(const Container &c) { + int size = int(c.size()); + if (size <= 0) + __testlib_fail("random_t::any(const Container& c): c.size() must be positive"); + typename Container::const_iterator it = c.begin(); + std::advance(it, next(size)); + return *it; + } + + /* Returns random element from iterator range. */ + template + typename Iter::value_type any(const Iter &begin, const Iter &end) { + int size = static_cast(std::distance(begin, end)); + if (size <= 0) + __testlib_fail("random_t::any(const Iter& begin, const Iter& end): range must have positive length"); + Iter it = begin; + std::advance(it, next(size)); + return *it; + } + + /* Random string value by given pattern (see pattern documentation). */ +#ifdef __GNUC__ + __attribute__ ((format (printf, 2, 3))) +#endif + std::string next(const char *format, ...) { + FMT_TO_RESULT(format, format, ptrn); + return next(ptrn); + } + + /* + * Weighted next. If type == 0 than it is usual "next()". + * + * If type = 1, than it returns "max(next(), next())" + * (the number of "max" functions equals to "type"). + * + * If type < 0, than "max" function replaces with "min". + */ + int wnext(int n, int type) { + if (n <= 0) + __testlib_fail("random_t::wnext(int n, int type): n must be positive"); + + if (abs(type) < random_t::lim) { + int result = next(n); + + for (int i = 0; i < +type; i++) + result = __testlib_max(result, next(n)); + + for (int i = 0; i < -type; i++) + result = __testlib_min(result, next(n)); + + return result; + } else { + double p; + + if (type > 0) + p = std::pow(next() + 0.0, 1.0 / (type + 1)); + else + p = 1 - std::pow(next() + 0.0, 1.0 / (-type + 1)); + + return __testlib_crop((int) (double(n) * p), 0, n); + } + } + + /* See wnext(int, int). It uses the same algorithms. */ + long long wnext(long long n, int type) { + if (n <= 0) + __testlib_fail("random_t::wnext(long long n, int type): n must be positive"); + + if (abs(type) < random_t::lim) { + long long result = next(n); + + for (int i = 0; i < +type; i++) + result = __testlib_max(result, next(n)); + + for (int i = 0; i < -type; i++) + result = __testlib_min(result, next(n)); + + return result; + } else { + double p; + + if (type > 0) + p = std::pow(next() + 0.0, 1.0 / (type + 1)); + else + p = 1 - std::pow(next() + 0.0, 1.0 / (-type + 1)); + + return __testlib_crop((long long) (double(n) * p), 0LL, n); + } + } + + /* Returns value in [0, n). See wnext(int, int). It uses the same algorithms. */ + double wnext(double n, int type) { + if (n <= 0) + __testlib_fail("random_t::wnext(double n, int type): n must be positive"); + + if (abs(type) < random_t::lim) { + double result = next(); + + for (int i = 0; i < +type; i++) + result = __testlib_max(result, next()); + + for (int i = 0; i < -type; i++) + result = __testlib_min(result, next()); + + return n * result; + } else { + double p; + + if (type > 0) + p = std::pow(next() + 0.0, 1.0 / (type + 1)); + else + p = 1 - std::pow(next() + 0.0, 1.0 / (-type + 1)); + + return __testlib_crop(n * p, 0.0, n); + } + } + + /* Returns value in [0, 1). See wnext(int, int). It uses the same algorithms. */ + double wnext(int type) { + return wnext(1.0, type); + } + + /* See wnext(int, int). It uses the same algorithms. */ + unsigned int wnext(unsigned int n, int type) { + if (n >= INT_MAX) + __testlib_fail("random_t::wnext(unsigned int n, int type): n must be less INT_MAX"); + return (unsigned int) wnext(int(n), type); + } + + /* See wnext(int, int). It uses the same algorithms. */ + unsigned long long wnext(unsigned long long n, int type) { + if (n >= (unsigned long long) (__TESTLIB_LONGLONG_MAX)) + __testlib_fail("random_t::wnext(unsigned long long n, int type): n must be less LONGLONG_MAX"); + + return (unsigned long long) wnext((long long) (n), type); + } + + /* See wnext(int, int). It uses the same algorithms. */ + long wnext(long n, int type) { + return (long) wnext((long long) (n), type); + } + + /* See wnext(int, int). It uses the same algorithms. */ + unsigned long wnext(unsigned long n, int type) { + if (n >= (unsigned long) (LONG_MAX)) + __testlib_fail("random_t::wnext(unsigned long n, int type): n must be less LONG_MAX"); + + return (unsigned long) wnext((unsigned long long) (n), type); + } + + /* Returns weighted random value in range [from, to]. */ + int wnext(int from, int to, int type) { + if (from > to) + __testlib_fail("random_t::wnext(int from, int to, int type): from can't not exceed to"); + return wnext(to - from + 1, type) + from; + } + + /* Returns weighted random value in range [from, to]. */ + int wnext(unsigned int from, unsigned int to, int type) { + if (from > to) + __testlib_fail("random_t::wnext(unsigned int from, unsigned int to, int type): from can't not exceed to"); + return int(wnext(to - from + 1, type) + from); + } + + /* Returns weighted random value in range [from, to]. */ + long long wnext(long long from, long long to, int type) { + if (from > to) + __testlib_fail("random_t::wnext(long long from, long long to, int type): from can't not exceed to"); + return wnext(to - from + 1, type) + from; + } + + /* Returns weighted random value in range [from, to]. */ + unsigned long long wnext(unsigned long long from, unsigned long long to, int type) { + if (from > to) + __testlib_fail( + "random_t::wnext(unsigned long long from, unsigned long long to, int type): from can't not exceed to"); + return wnext(to - from + 1, type) + from; + } + + /* Returns weighted random value in range [from, to]. */ + long wnext(long from, long to, int type) { + if (from > to) + __testlib_fail("random_t::wnext(long from, long to, int type): from can't not exceed to"); + return wnext(to - from + 1, type) + from; + } + + /* Returns weighted random value in range [from, to]. */ + unsigned long wnext(unsigned long from, unsigned long to, int type) { + if (from > to) + __testlib_fail("random_t::wnext(unsigned long from, unsigned long to, int type): from can't not exceed to"); + return wnext(to - from + 1, type) + from; + } + + /* Returns weighted random double value in range [from, to). */ + double wnext(double from, double to, int type) { + if (from >= to) + __testlib_fail("random_t::wnext(double from, double to, int type): from should be strictly less than to"); + return wnext(to - from, type) + from; + } + + /* Returns weighted random element from container. */ + template + typename Container::value_type wany(const Container &c, int type) { + int size = int(c.size()); + if (size <= 0) + __testlib_fail("random_t::wany(const Container& c, int type): c.size() must be positive"); + typename Container::const_iterator it = c.begin(); + std::advance(it, wnext(size, type)); + return *it; + } + + /* Returns weighted random element from iterator range. */ + template + typename Iter::value_type wany(const Iter &begin, const Iter &end, int type) { + int size = static_cast(std::distance(begin, end)); + if (size <= 0) + __testlib_fail( + "random_t::any(const Iter& begin, const Iter& end, int type): range must have positive length"); + Iter it = begin; + std::advance(it, wnext(size, type)); + return *it; + } + + /* Returns random permutation of the given size (values are between `first` and `first`+size-1)*/ + template + std::vector perm(T size, E first) { + if (size < 0) + __testlib_fail("random_t::perm(T size, E first = 0): size must non-negative"); + else if (size == 0) + return std::vector(); + std::vector p(size); + E current = first; + for (T i = 0; i < size; i++) + p[i] = current++; + if (size > 1) + for (T i = 1; i < size; i++) + std::swap(p[i], p[next(i + 1)]); + return p; + } + + /* Returns random permutation of the given size (values are between 0 and size-1)*/ + template + std::vector perm(T size) { + return perm(size, T(0)); + } + + /* Returns `size` unordered (unsorted) distinct numbers between `from` and `to`. */ + template + std::vector distinct(int size, T from, T to) { + std::vector result; + if (size == 0) + return result; + + if (from > to) + __testlib_fail("random_t::distinct expected from <= to"); + + if (size < 0) + __testlib_fail("random_t::distinct expected size >= 0"); + + uint64_t n = to - from + 1; + if (uint64_t(size) > n) + __testlib_fail("random_t::distinct expected size <= to - from + 1"); + + double expected = 0.0; + for (int i = 1; i <= size; i++) + expected += double(n) / double(n - i + 1); + + if (expected < double(n)) { + std::set vals; + while (int(vals.size()) < size) { + T x = T(next(from, to)); + if (vals.insert(x).second) + result.push_back(x); + } + } else { + if (n > 1000000000) + __testlib_fail("random_t::distinct here expected to - from + 1 <= 1000000000"); + std::vector p(perm(int(n), from)); + result.insert(result.end(), p.begin(), p.begin() + size); + } + + return result; + } + + /* Returns `size` unordered (unsorted) distinct numbers between `0` and `upper`-1. */ + template + std::vector distinct(int size, T upper) { + if (size < 0) + __testlib_fail("random_t::distinct expected size >= 0"); + if (size == 0) + return std::vector(); + + if (upper <= 0) + __testlib_fail("random_t::distinct expected upper > 0"); + if (size > upper) + __testlib_fail("random_t::distinct expected size <= upper"); + + return distinct(size, T(0), upper - 1); + } + + /* Returns random (unsorted) partition which is a representation of sum as a sum of integers not less than min_part. */ + template + std::vector partition(int size, T sum, T min_part) { + if (size < 0) + __testlib_fail("random_t::partition: size < 0"); + if (size == 0 && sum != 0) + __testlib_fail("random_t::partition: size == 0 && sum != 0"); + if (min_part * size > sum) + __testlib_fail("random_t::partition: min_part * size > sum"); + if (size == 0 && sum == 0) + return std::vector(); + + T sum_ = sum; + sum -= min_part * size; + + std::vector septums(size); + std::vector d = distinct(size - 1, T(1), T(sum + size - 1)); + for (int i = 0; i + 1 < size; i++) + septums[i + 1] = d[i]; + sort(septums.begin(), septums.end()); + + std::vector result(size); + for (int i = 0; i + 1 < size; i++) + result[i] = septums[i + 1] - septums[i] - 1; + result[size - 1] = sum + size - 1 - septums.back(); + + for (std::size_t i = 0; i < result.size(); i++) + result[i] += min_part; + + T result_sum = 0; + for (std::size_t i = 0; i < result.size(); i++) + result_sum += result[i]; + if (result_sum != sum_) + __testlib_fail("random_t::partition: partition sum is expected to be the given sum"); + + if (*std::min_element(result.begin(), result.end()) < min_part) + __testlib_fail("random_t::partition: partition min is expected to be no less than the given min_part"); + + if (int(result.size()) != size || result.size() != (size_t) size) + __testlib_fail("random_t::partition: partition size is expected to be equal to the given size"); + + return result; + } + + /* Returns random (unsorted) partition which is a representation of sum as a sum of positive integers. */ + template + std::vector partition(int size, T sum) { + return partition(size, sum, T(1)); + } +}; + +const int random_t::lim = 25; +const unsigned long long random_t::multiplier = 0x5DEECE66DLL; +const unsigned long long random_t::addend = 0xBLL; +const unsigned long long random_t::mask = (1LL << 48) - 1; +int random_t::version = -1; + +/* Pattern implementation */ +bool pattern::matches(const std::string &s) const { + return matches(s, 0); +} + +static bool __pattern_isSlash(const std::string &s, size_t pos) { + return s[pos] == '\\'; +} + +#ifdef __GNUC__ +__attribute__((pure)) +#endif +static bool __pattern_isCommandChar(const std::string &s, size_t pos, char value) { + if (pos >= s.length()) + return false; + + int slashes = 0; + + int before = int(pos) - 1; + while (before >= 0 && s[before] == '\\') + before--, slashes++; + + return slashes % 2 == 0 && s[pos] == value; +} + +static char __pattern_getChar(const std::string &s, size_t &pos) { + if (__pattern_isSlash(s, pos)) + pos += 2; + else + pos++; + + return s[pos - 1]; +} + +#ifdef __GNUC__ +__attribute__((pure)) +#endif +static int __pattern_greedyMatch(const std::string &s, size_t pos, const std::vector chars) { + int result = 0; + + while (pos < s.length()) { + char c = s[pos++]; + if (!std::binary_search(chars.begin(), chars.end(), c)) + break; + else + result++; + } + + return result; +} + +std::string pattern::src() const { + return s; +} + +bool pattern::matches(const std::string &s, size_t pos) const { + std::string result; + + if (to > 0) { + int size = __pattern_greedyMatch(s, pos, chars); + if (size < from) + return false; + if (size > to) + size = to; + pos += size; + } + + if (children.size() > 0) { + for (size_t child = 0; child < children.size(); child++) + if (children[child].matches(s, pos)) + return true; + return false; + } else + return pos == s.length(); +} + +std::string pattern::next(random_t &rnd) const { + std::string result; + result.reserve(20); + + if (to == INT_MAX) + __testlib_fail("pattern::next(random_t& rnd): can't process character '*' for generation"); + + if (to > 0) { + int count = rnd.next(to - from + 1) + from; + for (int i = 0; i < count; i++) + result += chars[rnd.next(int(chars.size()))]; + } + + if (children.size() > 0) { + int child = rnd.next(int(children.size())); + result += children[child].next(rnd); + } + + return result; +} + +static void __pattern_scanCounts(const std::string &s, size_t &pos, int &from, int &to) { + if (pos >= s.length()) { + from = to = 1; + return; + } + + if (__pattern_isCommandChar(s, pos, '{')) { + std::vector parts; + std::string part; + + pos++; + + while (pos < s.length() && !__pattern_isCommandChar(s, pos, '}')) { + if (__pattern_isCommandChar(s, pos, ',')) + parts.push_back(part), part = "", pos++; + else + part += __pattern_getChar(s, pos); + } + + if (part != "") + parts.push_back(part); + + if (!__pattern_isCommandChar(s, pos, '}')) + __testlib_fail("pattern: Illegal pattern (or part) \"" + s + "\""); + + pos++; + + if (parts.size() < 1 || parts.size() > 2) + __testlib_fail("pattern: Illegal pattern (or part) \"" + s + "\""); + + std::vector numbers; + + for (size_t i = 0; i < parts.size(); i++) { + if (parts[i].length() == 0) + __testlib_fail("pattern: Illegal pattern (or part) \"" + s + "\""); + int number; + if (std::sscanf(parts[i].c_str(), "%d", &number) != 1) + __testlib_fail("pattern: Illegal pattern (or part) \"" + s + "\""); + numbers.push_back(number); + } + + if (numbers.size() == 1) + from = to = numbers[0]; + else + from = numbers[0], to = numbers[1]; + + if (from > to) + __testlib_fail("pattern: Illegal pattern (or part) \"" + s + "\""); + } else { + if (__pattern_isCommandChar(s, pos, '?')) { + from = 0, to = 1, pos++; + return; + } + + if (__pattern_isCommandChar(s, pos, '*')) { + from = 0, to = INT_MAX, pos++; + return; + } + + if (__pattern_isCommandChar(s, pos, '+')) { + from = 1, to = INT_MAX, pos++; + return; + } + + from = to = 1; + } +} + +static std::vector __pattern_scanCharSet(const std::string &s, size_t &pos) { + if (pos >= s.length()) + __testlib_fail("pattern: Illegal pattern (or part) \"" + s + "\""); + + std::vector result; + + if (__pattern_isCommandChar(s, pos, '[')) { + pos++; + bool negative = __pattern_isCommandChar(s, pos, '^'); + if (negative) + pos++; + + char prev = 0; + + while (pos < s.length() && !__pattern_isCommandChar(s, pos, ']')) { + if (__pattern_isCommandChar(s, pos, '-') && prev != 0) { + pos++; + + if (pos + 1 == s.length() || __pattern_isCommandChar(s, pos, ']')) { + result.push_back(prev); + prev = '-'; + continue; + } + + char next = __pattern_getChar(s, pos); + if (prev > next) + __testlib_fail("pattern: Illegal pattern (or part) \"" + s + "\""); + + for (char c = prev; c != next; c++) + result.push_back(c); + result.push_back(next); + + prev = 0; + } else { + if (prev != 0) + result.push_back(prev); + prev = __pattern_getChar(s, pos); + } + } + + if (prev != 0) + result.push_back(prev); + + if (!__pattern_isCommandChar(s, pos, ']')) + __testlib_fail("pattern: Illegal pattern (or part) \"" + s + "\""); + + pos++; + + if (negative) { + std::sort(result.begin(), result.end()); + std::vector actuals; + for (int code = 0; code < 255; code++) { + char c = char(code); + if (!std::binary_search(result.begin(), result.end(), c)) + actuals.push_back(c); + } + result = actuals; + } + + std::sort(result.begin(), result.end()); + } else + result.push_back(__pattern_getChar(s, pos)); + + return result; +} + +pattern::pattern(std::string s) : s(s), from(0), to(0) { + std::string t; + for (size_t i = 0; i < s.length(); i++) + if (!__pattern_isCommandChar(s, i, ' ')) + t += s[i]; + s = t; + + int opened = 0; + int firstClose = -1; + std::vector seps; + + for (size_t i = 0; i < s.length(); i++) { + if (__pattern_isCommandChar(s, i, '(')) { + opened++; + continue; + } + + if (__pattern_isCommandChar(s, i, ')')) { + opened--; + if (opened == 0 && firstClose == -1) + firstClose = int(i); + continue; + } + + if (opened < 0) + __testlib_fail("pattern: Illegal pattern (or part) \"" + s + "\""); + + if (__pattern_isCommandChar(s, i, '|') && opened == 0) + seps.push_back(int(i)); + } + + if (opened != 0) + __testlib_fail("pattern: Illegal pattern (or part) \"" + s + "\""); + + if (seps.size() == 0 && firstClose + 1 == (int) s.length() + && __pattern_isCommandChar(s, 0, '(') && __pattern_isCommandChar(s, s.length() - 1, ')')) { + children.push_back(pattern(s.substr(1, s.length() - 2))); + } else { + if (seps.size() > 0) { + seps.push_back(int(s.length())); + int last = 0; + + for (size_t i = 0; i < seps.size(); i++) { + children.push_back(pattern(s.substr(last, seps[i] - last))); + last = seps[i] + 1; + } + } else { + size_t pos = 0; + chars = __pattern_scanCharSet(s, pos); + __pattern_scanCounts(s, pos, from, to); + if (pos < s.length()) + children.push_back(pattern(s.substr(pos))); + } + } +} + +/* End of pattern implementation */ + +template +inline bool isEof(C c) { + return c == EOFC; +} + +template +inline bool isEoln(C c) { + return (c == LF || c == CR); +} + +template +inline bool isBlanks(C c) { + return (c == LF || c == CR || c == SPACE || c == TAB); +} + +inline std::string trim(const std::string &s) { + if (s.empty()) + return s; + + int left = 0; + while (left < int(s.length()) && isBlanks(s[left])) + left++; + if (left >= int(s.length())) + return ""; + + int right = int(s.length()) - 1; + while (right >= 0 && isBlanks(s[right])) + right--; + if (right < 0) + return ""; + + return s.substr(left, right - left + 1); +} + +enum TMode { + _input, _output, _answer +}; + +/* Outcomes 6-15 are reserved for future use. */ +enum TResult { + _ok = 0, + _wa = 1, + _pe = 2, + _fail = 3, + _dirt = 4, + _points = 5, + _unexpected_eof = 8, + _partially = 16 +}; + +enum TTestlibMode { + _unknown, _checker, _validator, _generator, _interactor, _scorer +}; + +#define _pc(exitCode) (TResult(_partially + (exitCode))) + +/* Outcomes 6-15 are reserved for future use. */ +const std::string outcomes[] = { + "accepted", + "wrong-answer", + "presentation-error", + "fail", + "fail", +#ifndef PCMS2 + "points", +#else + "relative-scoring", +#endif + "reserved", + "reserved", + "unexpected-eof", + "reserved", + "reserved", + "reserved", + "reserved", + "reserved", + "reserved", + "reserved", + "partially-correct" +}; + +class InputStreamReader { +public: + virtual void setTestCase(int testCase) = 0; + + virtual std::vector getReadChars() = 0; + + virtual int curChar() = 0; + + virtual int nextChar() = 0; + + virtual void skipChar() = 0; + + virtual void unreadChar(int c) = 0; + + virtual std::string getName() = 0; + + virtual bool eof() = 0; + + virtual void close() = 0; + + virtual int getLine() = 0; + + virtual ~InputStreamReader() = 0; +}; + +InputStreamReader::~InputStreamReader() { + // No operations. +} + +class StringInputStreamReader : public InputStreamReader { +private: + std::string s; + size_t pos; + +public: + StringInputStreamReader(const std::string &content) : s(content), pos(0) { + // No operations. + } + + void setTestCase(int) { + __testlib_fail("setTestCase not implemented in StringInputStreamReader"); + } + + std::vector getReadChars() { + __testlib_fail("getReadChars not implemented in StringInputStreamReader"); + } + + int curChar() { + if (pos >= s.length()) + return EOFC; + else + return s[pos]; + } + + int nextChar() { + if (pos >= s.length()) { + pos++; + return EOFC; + } else + return s[pos++]; + } + + void skipChar() { + pos++; + } + + void unreadChar(int c) { + if (pos == 0) + __testlib_fail("StringInputStreamReader::unreadChar(int): pos == 0."); + pos--; + if (pos < s.length()) + s[pos] = char(c); + } + + std::string getName() { + return __testlib_part(s); + } + + int getLine() { + return -1; + } + + bool eof() { + return pos >= s.length(); + } + + void close() { + // No operations. + } +}; + +class FileInputStreamReader : public InputStreamReader { +private: + std::FILE *file; + std::string name; + int line; + std::vector undoChars; + std::vector readChars; + std::vector undoReadChars; + + inline int postprocessGetc(int getcResult) { + if (getcResult != EOF) + return getcResult; + else + return EOFC; + } + + int getc(FILE *file) { + int c; + int rc; + + if (undoChars.empty()) { + c = rc = ::getc(file); + } else { + c = undoChars.back(); + undoChars.pop_back(); + rc = undoReadChars.back(); + undoReadChars.pop_back(); + } + + if (c == LF) + line++; + + readChars.push_back(rc); + return c; + } + + int ungetc(int c/*, FILE* file*/) { + if (!readChars.empty()) { + undoReadChars.push_back(readChars.back()); + readChars.pop_back(); + } + if (c == LF) + line--; + undoChars.push_back(c); + return c; + } + +public: + FileInputStreamReader(std::FILE *file, const std::string &name) : file(file), name(name), line(1) { + // No operations. + } + + void setTestCase(int testCase) { + if (testCase < 0 || testCase > __TESTLIB_MAX_TEST_CASE) + __testlib_fail(format("testCase expected fit in [1,%d], but %d doesn't", __TESTLIB_MAX_TEST_CASE, testCase)); + readChars.push_back(testCase + 256); + } + + std::vector getReadChars() { + return readChars; + } + + int curChar() { + if (feof(file)) + return EOFC; + else { + int c = getc(file); + ungetc(c/*, file*/); + return postprocessGetc(c); + } + } + + int nextChar() { + if (feof(file)) + return EOFC; + else + return postprocessGetc(getc(file)); + } + + void skipChar() { + getc(file); + } + + void unreadChar(int c) { + ungetc(c/*, file*/); + } + + std::string getName() { + return name; + } + + int getLine() { + return line; + } + + bool eof() { + if (NULL == file || feof(file)) + return true; + else { + int c = nextChar(); + if (c == EOFC || (c == EOF && feof(file))) + return true; + unreadChar(c); + return false; + } + } + + void close() { + if (NULL != file) { + fclose(file); + file = NULL; + } + } +}; + +class BufferedFileInputStreamReader : public InputStreamReader { +private: + static const size_t BUFFER_SIZE; + static const size_t MAX_UNREAD_COUNT; + + std::FILE *file; + std::string name; + int line; + + char *buffer; + bool *isEof; + int bufferPos; + size_t bufferSize; + + bool refill() { + if (NULL == file) + __testlib_fail("BufferedFileInputStreamReader: file == NULL (" + getName() + ")"); + + if (bufferPos >= int(bufferSize)) { + size_t readSize = fread( + buffer + MAX_UNREAD_COUNT, + 1, + BUFFER_SIZE - MAX_UNREAD_COUNT, + file + ); + + if (readSize < BUFFER_SIZE - MAX_UNREAD_COUNT + && ferror(file)) + __testlib_fail("BufferedFileInputStreamReader: unable to read (" + getName() + ")"); + + bufferSize = MAX_UNREAD_COUNT + readSize; + bufferPos = int(MAX_UNREAD_COUNT); + std::memset(isEof + MAX_UNREAD_COUNT, 0, sizeof(isEof[0]) * readSize); + + return readSize > 0; + } else + return true; + } + + char increment() { + char c; + if ((c = buffer[bufferPos++]) == LF) + line++; + return c; + } + +public: + BufferedFileInputStreamReader(std::FILE *file, const std::string &name) : file(file), name(name), line(1) { + buffer = new char[BUFFER_SIZE]; + isEof = new bool[BUFFER_SIZE]; + bufferSize = MAX_UNREAD_COUNT; + bufferPos = int(MAX_UNREAD_COUNT); + } + + ~BufferedFileInputStreamReader() { + if (NULL != buffer) { + delete[] buffer; + buffer = NULL; + } + if (NULL != isEof) { + delete[] isEof; + isEof = NULL; + } + } + + void setTestCase(int) { + __testlib_fail("setTestCase not implemented in BufferedFileInputStreamReader"); + } + + std::vector getReadChars() { + __testlib_fail("getReadChars not implemented in BufferedFileInputStreamReader"); + } + + int curChar() { + if (!refill()) + return EOFC; + + return isEof[bufferPos] ? EOFC : buffer[bufferPos]; + } + + int nextChar() { + if (!refill()) + return EOFC; + + return isEof[bufferPos] ? EOFC : increment(); + } + + void skipChar() { + increment(); + } + + void unreadChar(int c) { + bufferPos--; + if (bufferPos < 0) + __testlib_fail("BufferedFileInputStreamReader::unreadChar(int): bufferPos < 0"); + isEof[bufferPos] = (c == EOFC); + buffer[bufferPos] = char(c); + if (c == LF) + line--; + } + + std::string getName() { + return name; + } + + int getLine() { + return line; + } + + bool eof() { + return !refill() || EOFC == curChar(); + } + + void close() { + if (NULL != file) { + fclose(file); + file = NULL; + } + } +}; + +const size_t BufferedFileInputStreamReader::BUFFER_SIZE = 2000000; +const size_t BufferedFileInputStreamReader::MAX_UNREAD_COUNT = BufferedFileInputStreamReader::BUFFER_SIZE / 2; + +/* + * Streams to be used for reading data in checkers or validators. + * Each read*() method moves pointer to the next character after the + * read value. + */ +struct InStream { + /* Do not use them. */ + InStream(); + + ~InStream(); + + /* Wrap std::string with InStream. */ + InStream(const InStream &baseStream, std::string content); + + InputStreamReader *reader; + int lastLine; + + std::string name; + TMode mode; + bool opened; + bool stdfile; + bool strict; + + int wordReserveSize; + std::string _tmpReadToken; + + int readManyIteration; + size_t maxFileSize; + size_t maxTokenLength; + size_t maxMessageLength; + + void init(std::string fileName, TMode mode); + + void init(std::FILE *f, TMode mode); + + void setTestCase(int testCase); + std::vector getReadChars(); + + /* Moves stream pointer to the first non-white-space character or EOF. */ + void skipBlanks(); + + /* Returns current character in the stream. Doesn't remove it from stream. */ + char curChar(); + + /* Moves stream pointer one character forward. */ + void skipChar(); + + /* Returns current character and moves pointer one character forward. */ + char nextChar(); + + /* Returns current character and moves pointer one character forward. */ + char readChar(); + + /* As "readChar()" but ensures that the result is equal to given parameter. */ + char readChar(char c); + + /* As "readChar()" but ensures that the result is equal to the space (code=32). */ + char readSpace(); + + /* Puts back the character into the stream. */ + void unreadChar(char c); + + /* Reopens stream, you should not use it. */ + void reset(std::FILE *file = NULL); + + /* Checks that current position is EOF. If not it doesn't move stream pointer. */ + bool eof(); + + /* Moves pointer to the first non-white-space character and calls "eof()". */ + bool seekEof(); + + /* + * Checks that current position contains EOLN. + * If not it doesn't move stream pointer. + * In strict mode expects "#13#10" for windows or "#10" for other platforms. + */ + bool eoln(); + + /* Moves pointer to the first non-space and non-tab character and calls "eoln()". */ + bool seekEoln(); + + /* Moves stream pointer to the first character of the next line (if exists). */ + void nextLine(); + + /* + * Reads new token. Ignores white-spaces into the non-strict mode + * (strict mode is used in validators usually). + */ + std::string readWord(); + + /* The same as "readWord()", it is preferred to use "readToken()". */ + std::string readToken(); + + /* The same as "readWord()", but ensures that token matches to given pattern. */ + std::string readWord(const std::string &ptrn, const std::string &variableName = ""); + + std::string readWord(const pattern &p, const std::string &variableName = ""); + + std::vector + readWords(int size, const std::string &ptrn, const std::string &variablesName = "", int indexBase = 1); + + std::vector + readWords(int size, const pattern &p, const std::string &variablesName = "", int indexBase = 1); + + std::vector readWords(int size, int indexBase = 1); + + /* The same as "readToken()", but ensures that token matches to given pattern. */ + std::string readToken(const std::string &ptrn, const std::string &variableName = ""); + + std::string readToken(const pattern &p, const std::string &variableName = ""); + + std::vector + readTokens(int size, const std::string &ptrn, const std::string &variablesName = "", int indexBase = 1); + + std::vector + readTokens(int size, const pattern &p, const std::string &variablesName = "", int indexBase = 1); + + std::vector readTokens(int size, int indexBase = 1); + + void readWordTo(std::string &result); + + void readWordTo(std::string &result, const pattern &p, const std::string &variableName = ""); + + void readWordTo(std::string &result, const std::string &ptrn, const std::string &variableName = ""); + + void readTokenTo(std::string &result); + + void readTokenTo(std::string &result, const pattern &p, const std::string &variableName = ""); + + void readTokenTo(std::string &result, const std::string &ptrn, const std::string &variableName = ""); + + /* + * Reads new long long value. Ignores white-spaces into the non-strict mode + * (strict mode is used in validators usually). + */ + long long readLong(); + + unsigned long long readUnsignedLong(); + + /* + * Reads new int. Ignores white-spaces into the non-strict mode + * (strict mode is used in validators usually). + */ + int readInteger(); + + /* + * Reads new int. Ignores white-spaces into the non-strict mode + * (strict mode is used in validators usually). + */ + int readInt(); + + /* As "readLong()" but ensures that value in the range [minv,maxv]. */ + long long readLong(long long minv, long long maxv, const std::string &variableName = ""); + + /* Reads space-separated sequence of long longs. */ + std::vector + readLongs(int size, long long minv, long long maxv, const std::string &variablesName = "", int indexBase = 1); + + /* Reads space-separated sequence of long longs. */ + std::vector readLongs(int size, int indexBase = 1); + + unsigned long long + readUnsignedLong(unsigned long long minv, unsigned long long maxv, const std::string &variableName = ""); + + std::vector + readUnsignedLongs(int size, unsigned long long minv, unsigned long long maxv, const std::string &variablesName = "", + int indexBase = 1); + + std::vector readUnsignedLongs(int size, int indexBase = 1); + + unsigned long long readLong(unsigned long long minv, unsigned long long maxv, const std::string &variableName = ""); + + std::vector + readLongs(int size, unsigned long long minv, unsigned long long maxv, const std::string &variablesName = "", + int indexBase = 1); + + /* As "readInteger()" but ensures that value in the range [minv,maxv]. */ + int readInteger(int minv, int maxv, const std::string &variableName = ""); + + /* As "readInt()" but ensures that value in the range [minv,maxv]. */ + int readInt(int minv, int maxv, const std::string &variableName = ""); + + /* Reads space-separated sequence of integers. */ + std::vector + readIntegers(int size, int minv, int maxv, const std::string &variablesName = "", int indexBase = 1); + + /* Reads space-separated sequence of integers. */ + std::vector readIntegers(int size, int indexBase = 1); + + /* Reads space-separated sequence of integers. */ + std::vector readInts(int size, int minv, int maxv, const std::string &variablesName = "", int indexBase = 1); + + /* Reads space-separated sequence of integers. */ + std::vector readInts(int size, int indexBase = 1); + + /* + * Reads new double. Ignores white-spaces into the non-strict mode + * (strict mode is used in validators usually). + */ + double readReal(); + + /* + * Reads new double. Ignores white-spaces into the non-strict mode + * (strict mode is used in validators usually). + */ + double readDouble(); + + /* As "readReal()" but ensures that value in the range [minv,maxv]. */ + double readReal(double minv, double maxv, const std::string &variableName = ""); + + std::vector + readReals(int size, double minv, double maxv, const std::string &variablesName = "", int indexBase = 1); + + std::vector readReals(int size, int indexBase = 1); + + /* As "readDouble()" but ensures that value in the range [minv,maxv]. */ + double readDouble(double minv, double maxv, const std::string &variableName = ""); + + std::vector + readDoubles(int size, double minv, double maxv, const std::string &variablesName = "", int indexBase = 1); + + std::vector readDoubles(int size, int indexBase = 1); + + /* + * As "readReal()" but ensures that value in the range [minv,maxv] and + * number of digit after the decimal point is in range [minAfterPointDigitCount,maxAfterPointDigitCount] + * and number is in the form "[-]digit(s)[.digit(s)]". + */ + double readStrictReal(double minv, double maxv, + int minAfterPointDigitCount, int maxAfterPointDigitCount, + const std::string &variableName = ""); + + std::vector readStrictReals(int size, double minv, double maxv, + int minAfterPointDigitCount, int maxAfterPointDigitCount, + const std::string &variablesName = "", int indexBase = 1); + + /* + * As "readDouble()" but ensures that value in the range [minv,maxv] and + * number of digit after the decimal point is in range [minAfterPointDigitCount,maxAfterPointDigitCount] + * and number is in the form "[-]digit(s)[.digit(s)]". + */ + double readStrictDouble(double minv, double maxv, + int minAfterPointDigitCount, int maxAfterPointDigitCount, + const std::string &variableName = ""); + + std::vector readStrictDoubles(int size, double minv, double maxv, + int minAfterPointDigitCount, int maxAfterPointDigitCount, + const std::string &variablesName = "", int indexBase = 1); + + /* As readLine(). */ + std::string readString(); + + /* Read many lines. */ + std::vector readStrings(int size, int indexBase = 1); + + /* See readLine(). */ + void readStringTo(std::string &result); + + /* The same as "readLine()/readString()", but ensures that line matches to the given pattern. */ + std::string readString(const pattern &p, const std::string &variableName = ""); + + /* The same as "readLine()/readString()", but ensures that line matches to the given pattern. */ + std::string readString(const std::string &ptrn, const std::string &variableName = ""); + + /* Read many lines. */ + std::vector + readStrings(int size, const pattern &p, const std::string &variableName = "", int indexBase = 1); + + /* Read many lines. */ + std::vector + readStrings(int size, const std::string &ptrn, const std::string &variableName = "", int indexBase = 1); + + /* The same as "readLine()/readString()", but ensures that line matches to the given pattern. */ + void readStringTo(std::string &result, const pattern &p, const std::string &variableName = ""); + + /* The same as "readLine()/readString()", but ensures that line matches to the given pattern. */ + void readStringTo(std::string &result, const std::string &ptrn, const std::string &variableName = ""); + + /* + * Reads line from the current position to EOLN or EOF. Moves stream pointer to + * the first character of the new line (if possible). + */ + std::string readLine(); + + /* Read many lines. */ + std::vector readLines(int size, int indexBase = 1); + + /* See readLine(). */ + void readLineTo(std::string &result); + + /* The same as "readLine()", but ensures that line matches to the given pattern. */ + std::string readLine(const pattern &p, const std::string &variableName = ""); + + /* The same as "readLine()", but ensures that line matches to the given pattern. */ + std::string readLine(const std::string &ptrn, const std::string &variableName = ""); + + /* Read many lines. */ + std::vector + readLines(int size, const pattern &p, const std::string &variableName = "", int indexBase = 1); + + /* Read many lines. */ + std::vector + readLines(int size, const std::string &ptrn, const std::string &variableName = "", int indexBase = 1); + + /* The same as "readLine()", but ensures that line matches to the given pattern. */ + void readLineTo(std::string &result, const pattern &p, const std::string &variableName = ""); + + /* The same as "readLine()", but ensures that line matches to the given pattern. */ + void readLineTo(std::string &result, const std::string &ptrn, const std::string &variableName = ""); + + /* Reads EOLN or fails. Use it in validators. Calls "eoln()" method internally. */ + void readEoln(); + + /* Reads EOF or fails. Use it in validators. Calls "eof()" method internally. */ + void readEof(); + + /* + * Quit-functions aborts program with and : + * input/answer streams replace any result to FAIL. + */ + NORETURN void quit(TResult result, const char *msg); + /* + * Quit-functions aborts program with and : + * input/answer streams replace any result to FAIL. + */ + NORETURN void quitf(TResult result, const char *msg, ...); + + /* + * Quit-functions aborts program with and : + * input/answer streams replace any result to FAIL. + */ + void quitif(bool condition, TResult result, const char *msg, ...); + /* + * Quit-functions aborts program with and : + * input/answer streams replace any result to FAIL. + */ + NORETURN void quits(TResult result, std::string msg); + + /* + * Checks condition and aborts a program if condition is false. + * Returns _wa for ouf and _fail on any other streams. + */ +#ifdef __GNUC__ + __attribute__ ((format (printf, 3, 4))) +#endif + void ensuref(bool cond, const char *format, ...); + + void __testlib_ensure(bool cond, std::string message); + + void close(); + + const static int NO_INDEX = INT_MAX; + const static char OPEN_BRACKET = char(11); + const static char CLOSE_BRACKET = char(17); + + const static WORD LightGray = 0x07; + const static WORD LightRed = 0x0c; + const static WORD LightCyan = 0x0b; + const static WORD LightGreen = 0x0a; + const static WORD LightYellow = 0x0e; + + static void textColor(WORD color); + + static void quitscr(WORD color, const char *msg); + + static void quitscrS(WORD color, std::string msg); + + void xmlSafeWrite(std::FILE *file, const char *msg); + + /* Skips UTF-8 Byte Order Mark. */ + void skipBom(); + +private: + InStream(const InStream &); + + InStream &operator=(const InStream &); +}; + +InStream inf; +InStream ouf; +InStream ans; +bool appesMode; +std::string appesModeEncoding = "windows-1251"; +std::string resultName; +std::string checkerName = "untitled checker"; +random_t rnd; +TTestlibMode testlibMode = _unknown; +double __testlib_points = std::numeric_limits::infinity(); + +const size_t VALIDATOR_MAX_VARIABLE_COUNT = 255; + +struct ValidatorBoundsHit { + static const double EPS; + bool minHit; + bool maxHit; + + ValidatorBoundsHit(bool minHit = false, bool maxHit = false) : minHit(minHit), maxHit(maxHit) { + }; + + ValidatorBoundsHit merge(const ValidatorBoundsHit &validatorBoundsHit, bool ignoreMinBound, bool ignoreMaxBound) { + return ValidatorBoundsHit( + __testlib_max(minHit, validatorBoundsHit.minHit) || ignoreMinBound, + __testlib_max(maxHit, validatorBoundsHit.maxHit) || ignoreMaxBound + ); + } +}; + +struct ConstantBound { + std::string value; + bool broken; + + template + void adjust(T t) { + std::string t_string = std::to_string(t); + if (t_string.length() >= 32) { + broken = true; + value = ""; + } else { + if (!broken && value.empty()) + value = t_string; + if (!broken && value != t_string) { + broken = true; + value = ""; + } + } + } + + bool has_value() { + return !value.empty() && !broken && value.length() < 32; + } +}; + +struct ConstantBounds { + ConstantBound lowerBound; + ConstantBound upperBound; +}; + +const double ValidatorBoundsHit::EPS = 1E-12; + +class Validator { +private: + const static std::string TEST_MARKUP_HEADER; + const static std::string TEST_CASE_OPEN_TAG; + const static std::string TEST_CASE_CLOSE_TAG; + + bool _initialized; + std::string _testset; + std::string _group; + + std::string _testOverviewLogFileName; + std::string _testMarkupFileName; + int _testCase = -1; + std::string _testCaseFileName; + + std::map _boundsHitByVariableName; + std::map _constantBoundsByVariableName; + std::set _features; + std::set _hitFeatures; + std::set _variables; + + bool isVariableNameBoundsAnalyzable(const std::string &variableName) { + for (size_t i = 0; i < variableName.length(); i++) + if ((variableName[i] >= '0' && variableName[i] <= '9') || variableName[i] < ' ') + return false; + return true; + } + + bool isFeatureNameAnalyzable(const std::string &featureName) { + for (size_t i = 0; i < featureName.length(); i++) + if (featureName[i] < ' ') + return false; + return true; + } + +public: + Validator() : _initialized(false), _testset("tests"), _group() { + } + + void initialize() { + _initialized = true; + } + + std::string testset() const { + if (!_initialized) + __testlib_fail("Validator should be initialized with registerValidation(argc, argv) instead of registerValidation() to support validator.testset()"); + return _testset; + } + + std::string group() const { + if (!_initialized) + __testlib_fail("Validator should be initialized with registerValidation(argc, argv) instead of registerValidation() to support validator.group()"); + return _group; + } + + std::string testOverviewLogFileName() const { + return _testOverviewLogFileName; + } + + std::string testMarkupFileName() const { + return _testMarkupFileName; + } + + int testCase() const { + return _testCase; + } + + std::string testCaseFileName() const { + return _testCaseFileName; + } + + void setTestset(const char *const testset) { + _testset = testset; + } + + void setGroup(const char *const group) { + _group = group; + } + + void setTestOverviewLogFileName(const char *const testOverviewLogFileName) { + _testOverviewLogFileName = testOverviewLogFileName; + } + + void setTestMarkupFileName(const char *const testMarkupFileName) { + _testMarkupFileName = testMarkupFileName; + } + + void setTestCase(int testCase) { + _testCase = testCase; + } + + void setTestCaseFileName(const char *const testCaseFileName) { + _testCaseFileName = testCaseFileName; + } + + std::string prepVariableName(const std::string &variableName) { + if (variableName.length() >= 2 && variableName != "~~") { + if (variableName[0] == '~' && variableName.back() != '~') + return variableName.substr(1); + if (variableName[0] != '~' && variableName.back() == '~') + return variableName.substr(0, variableName.length() - 1); + if (variableName[0] == '~' && variableName.back() == '~') + return variableName.substr(1, variableName.length() - 2); + } + return variableName; + } + + bool ignoreMinBound(const std::string &variableName) { + return variableName.length() >= 2 && variableName != "~~" && variableName[0] == '~'; + } + + bool ignoreMaxBound(const std::string &variableName) { + return variableName.length() >= 2 && variableName != "~~" && variableName.back() == '~'; + } + + void addBoundsHit(const std::string &variableName, ValidatorBoundsHit boundsHit) { + if (isVariableNameBoundsAnalyzable(variableName) + && _boundsHitByVariableName.size() < VALIDATOR_MAX_VARIABLE_COUNT) { + std::string preparedVariableName = prepVariableName(variableName); + _boundsHitByVariableName[preparedVariableName] = boundsHit.merge(_boundsHitByVariableName[preparedVariableName], + ignoreMinBound(variableName), ignoreMaxBound(variableName)); + } + } + + void addVariable(const std::string &variableName) { + if (isVariableNameBoundsAnalyzable(variableName) + && _variables.size() < VALIDATOR_MAX_VARIABLE_COUNT) { + std::string preparedVariableName = prepVariableName(variableName); + _variables.insert(preparedVariableName); + } + } + + std::string getVariablesLog() { + std::string result; + for (const std::string &variableName: _variables) + result += "variable \"" + variableName + "\"\n"; + return result; + } + + template + void adjustConstantBounds(const std::string &variableName, T lower, T upper) { + if (isVariableNameBoundsAnalyzable(variableName) + && _constantBoundsByVariableName.size() < VALIDATOR_MAX_VARIABLE_COUNT) { + std::string preparedVariableName = prepVariableName(variableName); + _constantBoundsByVariableName[preparedVariableName].lowerBound.adjust(lower); + _constantBoundsByVariableName[preparedVariableName].upperBound.adjust(upper); + } + } + + std::string getBoundsHitLog() { + std::string result; + for (std::map::iterator i = _boundsHitByVariableName.begin(); + i != _boundsHitByVariableName.end(); + i++) { + result += "\"" + i->first + "\":"; + if (i->second.minHit) + result += " min-value-hit"; + if (i->second.maxHit) + result += " max-value-hit"; + result += "\n"; + } + return result; + } + + std::string getConstantBoundsLog() { + std::string result; + for (std::map::iterator i = _constantBoundsByVariableName.begin(); + i != _constantBoundsByVariableName.end(); + i++) { + if (i->second.lowerBound.has_value() || i->second.upperBound.has_value()) { + result += "constant-bounds \"" + i->first + "\":"; + if (i->second.lowerBound.has_value()) + result += " " + i->second.lowerBound.value; + else + result += " ?"; + if (i->second.upperBound.has_value()) + result += " " + i->second.upperBound.value; + else + result += " ?"; + result += "\n"; + } + } + return result; + } + + std::string getFeaturesLog() { + std::string result; + for (std::set::iterator i = _features.begin(); + i != _features.end(); + i++) { + result += "feature \"" + *i + "\":"; + if (_hitFeatures.count(*i)) + result += " hit"; + result += "\n"; + } + return result; + } + + void writeTestOverviewLog() { + if (!_testOverviewLogFileName.empty()) { + std::string fileName(_testOverviewLogFileName); + _testOverviewLogFileName = ""; + + FILE* f; + bool standard_file = false; + if (fileName == "stdout") + f = stdout, standard_file = true; + else if (fileName == "stderr") + f = stderr, standard_file = true; + else { + f = fopen(fileName.c_str(), "wb"); + if (NULL == f) + __testlib_fail("Validator::writeTestOverviewLog: can't write test overview log to (" + fileName + ")"); + } + fprintf(f, "%s%s%s%s", + getBoundsHitLog().c_str(), + getFeaturesLog().c_str(), + getConstantBoundsLog().c_str(), + getVariablesLog().c_str()); + std::fflush(f); + if (!standard_file) + if (std::fclose(f)) + __testlib_fail("Validator::writeTestOverviewLog: can't close test overview log file (" + fileName + ")"); + } + } + + void writeTestMarkup() { + if (!_testMarkupFileName.empty()) { + std::vector readChars = inf.getReadChars(); + if (!readChars.empty()) { + std::string markup(TEST_MARKUP_HEADER); + for (size_t i = 0; i < readChars.size(); i++) { + int c = readChars[i]; + if (i + 1 == readChars.size() && c == -1) + continue; + if (c <= 256) { + char cc = char(c); + if (cc == '\\' || cc == '!') + markup += '\\'; + markup += cc; + } else { + markup += TEST_CASE_OPEN_TAG; + markup += toString(c - 256); + markup += TEST_CASE_CLOSE_TAG; + } + } + FILE* f; + bool standard_file = false; + if (_testMarkupFileName == "stdout") + f = stdout, standard_file = true; + else if (_testMarkupFileName == "stderr") + f = stderr, standard_file = true; + else { + f = fopen(_testMarkupFileName.c_str(), "wb"); + if (NULL == f) + __testlib_fail("Validator::writeTestMarkup: can't write test markup to (" + _testMarkupFileName + ")"); + } + std::fprintf(f, "%s", markup.c_str()); + std::fflush(f); + if (!standard_file) + if (std::fclose(f)) + __testlib_fail("Validator::writeTestMarkup: can't close test markup file (" + _testCaseFileName + ")"); + } + } + } + + void writeTestCase() { + if (_testCase > 0) { + std::vector readChars = inf.getReadChars(); + if (!readChars.empty()) { + std::string content, testCaseContent; + bool matchedTestCase = false; + for (size_t i = 0; i < readChars.size(); i++) { + int c = readChars[i]; + if (i + 1 == readChars.size() && c == -1) + continue; + if (c <= 256) + content += char(c); + else { + if (matchedTestCase) { + testCaseContent = content; + matchedTestCase = false; + } + content = ""; + int testCase = c - 256; + if (testCase == _testCase) + matchedTestCase = true; + } + } + if (matchedTestCase) + testCaseContent = content; + + if (!testCaseContent.empty()) { + FILE* f; + bool standard_file = false; + if (_testCaseFileName.empty() || _testCaseFileName == "stdout") + f = stdout, standard_file = true; + else if (_testCaseFileName == "stderr") + f = stderr, standard_file = true; + else { + f = fopen(_testCaseFileName.c_str(), "wb"); + if (NULL == f) + __testlib_fail("Validator::writeTestCase: can't write test case to (" + _testCaseFileName + ")"); + } + std::fprintf(f, "%s", testCaseContent.c_str()); + std::fflush(f); + if (!standard_file) + if (std::fclose(f)) + __testlib_fail("Validator::writeTestCase: can't close test case file (" + _testCaseFileName + ")"); + } + } + } + } + + void addFeature(const std::string &feature) { + if (_features.count(feature)) + __testlib_fail("Feature " + feature + " registered twice."); + if (!isFeatureNameAnalyzable(feature)) + __testlib_fail("Feature name '" + feature + "' contains restricted characters."); + + _features.insert(feature); + } + + void feature(const std::string &feature) { + if (!isFeatureNameAnalyzable(feature)) + __testlib_fail("Feature name '" + feature + "' contains restricted characters."); + + if (!_features.count(feature)) + __testlib_fail("Feature " + feature + " didn't registered via addFeature(feature)."); + + _hitFeatures.insert(feature); + } +} validator; + +const std::string Validator::TEST_MARKUP_HEADER = "MU\xF3\x01"; +const std::string Validator::TEST_CASE_OPEN_TAG = "!c"; +const std::string Validator::TEST_CASE_CLOSE_TAG = ";"; + +struct TestlibFinalizeGuard { + static bool alive; + static bool registered; + + int quitCount, readEofCount; + + TestlibFinalizeGuard() : quitCount(0), readEofCount(0) { + // No operations. + } + + ~TestlibFinalizeGuard() { + bool _alive = alive; + alive = false; + + if (_alive) { + if (testlibMode == _checker && quitCount == 0) + __testlib_fail("Checker must end with quit or quitf call."); + + if (testlibMode == _validator && readEofCount == 0 && quitCount == 0) + __testlib_fail("Validator must end with readEof call."); + + /* opts */ + autoEnsureNoUnusedOpts(); + + if (!registered) + __testlib_fail("Call register-function in the first line of the main (registerTestlibCmd or other similar)"); + } + + if (__testlib_exitCode == 0) { + validator.writeTestOverviewLog(); + validator.writeTestMarkup(); + validator.writeTestCase(); + } + } + +private: + /* opts */ + void autoEnsureNoUnusedOpts(); +}; + +bool TestlibFinalizeGuard::alive = true; +bool TestlibFinalizeGuard::registered = false; +extern TestlibFinalizeGuard testlibFinalizeGuard; + +/* + * Call it to disable checks on finalization. + */ +void disableFinalizeGuard() { + TestlibFinalizeGuard::alive = false; +} + +/* Interactor streams. + */ +std::fstream tout; + +/* implementation + */ + +InStream::InStream() { + reader = NULL; + lastLine = -1; + opened = false; + name = ""; + mode = _input; + strict = false; + stdfile = false; + wordReserveSize = 4; + readManyIteration = NO_INDEX; + maxFileSize = 128 * 1024 * 1024; // 128MB. + maxTokenLength = 32 * 1024 * 1024; // 32MB. + maxMessageLength = 32000; +} + +InStream::InStream(const InStream &baseStream, std::string content) { + reader = new StringInputStreamReader(content); + lastLine = -1; + opened = true; + strict = baseStream.strict; + stdfile = false; + mode = baseStream.mode; + name = "based on " + baseStream.name; + readManyIteration = NO_INDEX; + maxFileSize = 128 * 1024 * 1024; // 128MB. + maxTokenLength = 32 * 1024 * 1024; // 32MB. + maxMessageLength = 32000; +} + +InStream::~InStream() { + if (NULL != reader) { + reader->close(); + delete reader; + reader = NULL; + } +} + +void InStream::setTestCase(int testCase) { + if (testlibMode != _validator || mode != _input || !stdfile || this != &inf) + __testlib_fail("InStream::setTestCase can be used only for inf in validator-mode." + " Actually, prefer setTestCase function instead of InStream member"); + reader->setTestCase(testCase); +} + +std::vector InStream::getReadChars() { + if (testlibMode != _validator || mode != _input || !stdfile || this != &inf) + __testlib_fail("InStream::getReadChars can be used only for inf in validator-mode."); + return reader == NULL ? std::vector() : reader->getReadChars(); +} + +void setTestCase(int testCase) { + static bool first_run = true; + static bool zero_based = false; + + if (first_run && testCase == 0) + zero_based = true; + + if (zero_based) + testCase++; + + __testlib_hasTestCase = true; + __testlib_testCase = testCase; + + if (testlibMode == _validator) + inf.setTestCase(testCase); + + first_run = false; +} + +#ifdef __GNUC__ +__attribute__((const)) +#endif +int resultExitCode(TResult r) { + if (r == _ok) + return OK_EXIT_CODE; + if (r == _wa) + return WA_EXIT_CODE; + if (r == _pe) + return PE_EXIT_CODE; + if (r == _fail) + return FAIL_EXIT_CODE; + if (r == _dirt) + return DIRT_EXIT_CODE; + if (r == _points) + return POINTS_EXIT_CODE; + if (r == _unexpected_eof) +#ifdef ENABLE_UNEXPECTED_EOF + return UNEXPECTED_EOF_EXIT_CODE; +#else + return PE_EXIT_CODE; +#endif + if (r >= _partially) + return PC_BASE_EXIT_CODE + (r - _partially); + return FAIL_EXIT_CODE; +} + +void InStream::textColor( +#if !(defined(ON_WINDOWS) && (!defined(_MSC_VER) || _MSC_VER > 1400)) && defined(__GNUC__) + __attribute__((unused)) +#endif + WORD color +) { +#if defined(ON_WINDOWS) && (!defined(_MSC_VER) || _MSC_VER > 1400) + HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); + SetConsoleTextAttribute(handle, color); +#endif +#if !defined(ON_WINDOWS) && defined(__GNUC__) + if (isatty(2)) + { + switch (color) + { + case LightRed: + fprintf(stderr, "\033[1;31m"); + break; + case LightCyan: + fprintf(stderr, "\033[1;36m"); + break; + case LightGreen: + fprintf(stderr, "\033[1;32m"); + break; + case LightYellow: + fprintf(stderr, "\033[1;33m"); + break; + case LightGray: + default: + fprintf(stderr, "\033[0m"); + } + } +#endif +} + +#ifdef TESTLIB_THROW_EXIT_EXCEPTION_INSTEAD_OF_EXIT +class exit_exception: public std::exception { +private: + int exitCode; +public: + exit_exception(int exitCode): exitCode(exitCode) {} + int getExitCode() { return exitCode; } +}; +#endif + +NORETURN void halt(int exitCode) { +#ifdef FOOTER + InStream::textColor(InStream::LightGray); + std::fprintf(stderr, "Checker: \"%s\"\n", checkerName.c_str()); + std::fprintf(stderr, "Exit code: %d\n", exitCode); + InStream::textColor(InStream::LightGray); +#endif + __testlib_exitCode = exitCode; +#ifdef TESTLIB_THROW_EXIT_EXCEPTION_INSTEAD_OF_EXIT + throw exit_exception(exitCode); +#endif + std::exit(exitCode); +} + +static bool __testlib_shouldCheckDirt(TResult result) { + return result == _ok || result == _points || result >= _partially; +} + +static std::string __testlib_appendMessage(const std::string &message, const std::string &extra) { + int openPos = -1, closePos = -1; + for (size_t i = 0; i < message.length(); i++) { + if (message[i] == InStream::OPEN_BRACKET) { + if (openPos == -1) + openPos = int(i); + else + openPos = INT_MAX; + } + if (message[i] == InStream::CLOSE_BRACKET) { + if (closePos == -1) + closePos = int(i); + else + closePos = INT_MAX; + } + } + if (openPos != -1 && openPos != INT_MAX + && closePos != -1 && closePos != INT_MAX + && openPos < closePos) { + size_t index = message.find(extra, openPos); + if (index == std::string::npos || int(index) >= closePos) { + std::string result(message); + result.insert(closePos, ", " + extra); + return result; + } + return message; + } + + return message + " " + InStream::OPEN_BRACKET + extra + InStream::CLOSE_BRACKET; +} + +static std::string __testlib_toPrintableMessage(const std::string &message) { + int openPos = -1, closePos = -1; + for (size_t i = 0; i < message.length(); i++) { + if (message[i] == InStream::OPEN_BRACKET) { + if (openPos == -1) + openPos = int(i); + else + openPos = INT_MAX; + } + if (message[i] == InStream::CLOSE_BRACKET) { + if (closePos == -1) + closePos = int(i); + else + closePos = INT_MAX; + } + } + if (openPos != -1 && openPos != INT_MAX + && closePos != -1 && closePos != INT_MAX + && openPos < closePos) { + std::string result(message); + result[openPos] = '('; + result[closePos] = ')'; + return result; + } + + return message; +} + +NORETURN void InStream::quit(TResult result, const char *msg) { + if (TestlibFinalizeGuard::alive) + testlibFinalizeGuard.quitCount++; + + std::string message(msg); + message = trim(message); + + if (__testlib_hasTestCase) { + if (result != _ok) + message = __testlib_appendMessage(message, "test case " + vtos(__testlib_testCase)); + else { + if (__testlib_testCase == 1) + message = __testlib_appendMessage(message, vtos(__testlib_testCase) + " test case"); + else + message = __testlib_appendMessage(message, vtos(__testlib_testCase) + " test cases"); + } + } + + // You can change maxMessageLength. + // Example: 'inf.maxMessageLength = 1024 * 1024;'. + if (message.length() > maxMessageLength) { + std::string warn = "message length exceeds " + vtos(maxMessageLength) + + ", the message is truncated: "; + message = warn + message.substr(0, maxMessageLength - warn.length()); + } + +#ifndef ENABLE_UNEXPECTED_EOF + if (result == _unexpected_eof) + result = _pe; +#endif + + if (testlibMode == _scorer && result != _fail) + quits(_fail, "Scorer should return points only. Don't use a quit function."); + + if (mode != _output && result != _fail) { + if (mode == _input && testlibMode == _validator && lastLine != -1) + quits(_fail, __testlib_appendMessage(__testlib_appendMessage(message, name), "line " + vtos(lastLine))); + else + quits(_fail, __testlib_appendMessage(message, name)); + } + + std::FILE *resultFile; + std::string errorName; + + if (__testlib_shouldCheckDirt(result)) { + if (testlibMode != _interactor && !ouf.seekEof()) + quit(_dirt, "Extra information in the output file"); + } + + int pctype = result - _partially; + bool isPartial = false; + + switch (result) { + case _ok: + errorName = "ok "; + quitscrS(LightGreen, errorName); + break; + case _wa: + errorName = "wrong answer "; + quitscrS(LightRed, errorName); + break; + case _pe: + errorName = "wrong output format "; + quitscrS(LightRed, errorName); + break; + case _fail: + errorName = "FAIL "; + quitscrS(LightRed, errorName); + break; + case _dirt: + errorName = "wrong output format "; + quitscrS(LightCyan, errorName); + result = _pe; + break; + case _points: + errorName = "points "; + quitscrS(LightYellow, errorName); + break; + case _unexpected_eof: + errorName = "unexpected eof "; + quitscrS(LightCyan, errorName); + break; + default: + if (result >= _partially) { + errorName = format("partially correct (%d) ", pctype); + isPartial = true; + quitscrS(LightYellow, errorName); + } else + quit(_fail, "What is the code ??? "); + } + + if (resultName != "") { + resultFile = std::fopen(resultName.c_str(), "w"); + if (resultFile == NULL) { + resultName = ""; + quit(_fail, "Can not write to the result file"); + } + if (appesMode) { + std::fprintf(resultFile, "", appesModeEncoding.c_str()); + if (isPartial) + std::fprintf(resultFile, "", + outcomes[(int) _partially].c_str(), pctype); + else { + if (result != _points) + std::fprintf(resultFile, "", outcomes[(int) result].c_str()); + else { + if (__testlib_points == std::numeric_limits::infinity()) + quit(_fail, "Expected points, but infinity found"); + std::string stringPoints = removeDoubleTrailingZeroes(format("%.10f", __testlib_points)); + std::fprintf(resultFile, "", + outcomes[(int) result].c_str(), stringPoints.c_str()); + } + } + xmlSafeWrite(resultFile, __testlib_toPrintableMessage(message).c_str()); + std::fprintf(resultFile, "\n"); + } else + std::fprintf(resultFile, "%s", __testlib_toPrintableMessage(message).c_str()); + if (NULL == resultFile || fclose(resultFile) != 0) { + resultName = ""; + quit(_fail, "Can not write to the result file"); + } + } + + quitscr(LightGray, __testlib_toPrintableMessage(message).c_str()); + std::fprintf(stderr, "\n"); + + inf.close(); + ouf.close(); + ans.close(); + if (tout.is_open()) + tout.close(); + + textColor(LightGray); + + if (resultName != "") + std::fprintf(stderr, "See file to check exit message\n"); + + halt(resultExitCode(result)); +} + +#ifdef __GNUC__ +__attribute__ ((format (printf, 3, 4))) +#endif +NORETURN void InStream::quitf(TResult result, const char *msg, ...) { + FMT_TO_RESULT(msg, msg, message); + InStream::quit(result, message.c_str()); +} + +#ifdef __GNUC__ +__attribute__ ((format (printf, 4, 5))) +#endif +void InStream::quitif(bool condition, TResult result, const char *msg, ...) { + if (condition) { + FMT_TO_RESULT(msg, msg, message); + InStream::quit(result, message.c_str()); + } +} + +NORETURN void InStream::quits(TResult result, std::string msg) { + InStream::quit(result, msg.c_str()); +} + +void InStream::xmlSafeWrite(std::FILE *file, const char *msg) { + size_t lmsg = strlen(msg); + for (size_t i = 0; i < lmsg; i++) { + if (msg[i] == '&') { + std::fprintf(file, "%s", "&"); + continue; + } + if (msg[i] == '<') { + std::fprintf(file, "%s", "<"); + continue; + } + if (msg[i] == '>') { + std::fprintf(file, "%s", ">"); + continue; + } + if (msg[i] == '"') { + std::fprintf(file, "%s", """); + continue; + } + if (0 <= msg[i] && msg[i] <= 31) { + std::fprintf(file, "%c", '.'); + continue; + } + std::fprintf(file, "%c", msg[i]); + } +} + +void InStream::quitscrS(WORD color, std::string msg) { + quitscr(color, msg.c_str()); +} + +void InStream::quitscr(WORD color, const char *msg) { + if (resultName == "") { + textColor(color); + std::fprintf(stderr, "%s", msg); + textColor(LightGray); + } +} + +void InStream::reset(std::FILE *file) { + if (opened && stdfile) + quit(_fail, "Can't reset standard handle"); + + if (opened) + close(); + + if (!stdfile && NULL == file) + if (NULL == (file = std::fopen(name.c_str(), "rb"))) { + if (mode == _output) + quits(_pe, std::string("Output file not found: \"") + name + "\""); + + if (mode == _answer) + quits(_fail, std::string("Answer file not found: \"") + name + "\""); + } + + if (NULL != file) { + opened = true; + __testlib_set_binary(file); + + if (stdfile) + reader = new FileInputStreamReader(file, name); + else + reader = new BufferedFileInputStreamReader(file, name); + } else { + opened = false; + reader = NULL; + } +} + +void InStream::init(std::string fileName, TMode mode) { + opened = false; + name = fileName; + stdfile = false; + this->mode = mode; + + std::ifstream stream; + stream.open(fileName.c_str(), std::ios::in); + if (stream.is_open()) { + std::streampos start = stream.tellg(); + stream.seekg(0, std::ios::end); + std::streampos end = stream.tellg(); + size_t fileSize = size_t(end - start); + stream.close(); + + // You can change maxFileSize. + // Example: 'inf.maxFileSize = 256 * 1024 * 1024;'. + if (fileSize > maxFileSize) + quitf(_pe, "File size exceeds %d bytes, size is %d", int(maxFileSize), int(fileSize)); + } + + reset(); +} + +void InStream::init(std::FILE *f, TMode mode) { + opened = false; + name = "untitled"; + this->mode = mode; + + if (f == stdin) + name = "stdin", stdfile = true; + if (f == stdout) + name = "stdout", stdfile = true; + if (f == stderr) + name = "stderr", stdfile = true; + + reset(f); +} + +void InStream::skipBom() { + const std::string utf8Bom = "\xEF\xBB\xBF"; + size_t index = 0; + while (index < utf8Bom.size() && curChar() == utf8Bom[index]) { + index++; + skipChar(); + } + if (index < utf8Bom.size()) { + while (index != 0) { + unreadChar(utf8Bom[index - 1]); + index--; + } + } +} + +char InStream::curChar() { + return char(reader->curChar()); +} + +char InStream::nextChar() { + return char(reader->nextChar()); +} + +char InStream::readChar() { + return nextChar(); +} + +char InStream::readChar(char c) { + lastLine = reader->getLine(); + char found = readChar(); + if (c != found) { + if (!isEoln(found)) + quit(_pe, ("Unexpected character '" + std::string(1, found) + "', but '" + std::string(1, c) + + "' expected").c_str()); + else + quit(_pe, ("Unexpected character " + ("#" + vtos(int(found))) + ", but '" + std::string(1, c) + + "' expected").c_str()); + } + return found; +} + +char InStream::readSpace() { + return readChar(' '); +} + +void InStream::unreadChar(char c) { + reader->unreadChar(c); +} + +void InStream::skipChar() { + reader->skipChar(); +} + +void InStream::skipBlanks() { + while (isBlanks(reader->curChar())) + reader->skipChar(); +} + +std::string InStream::readWord() { + readWordTo(_tmpReadToken); + return _tmpReadToken; +} + +void InStream::readWordTo(std::string &result) { + if (!strict) + skipBlanks(); + + lastLine = reader->getLine(); + int cur = reader->nextChar(); + + if (cur == EOFC) + quit(_unexpected_eof, "Unexpected end of file - token expected"); + + if (isBlanks(cur)) + quit(_pe, "Unexpected white-space - token expected"); + + result.clear(); + + while (!(isBlanks(cur) || cur == EOFC)) { + result += char(cur); + + // You can change maxTokenLength. + // Example: 'inf.maxTokenLength = 128 * 1024 * 1024;'. + if (result.length() > maxTokenLength) + quitf(_pe, "Length of token exceeds %d, token is '%s...'", int(maxTokenLength), + __testlib_part(result).c_str()); + + cur = reader->nextChar(); + } + + reader->unreadChar(cur); + + if (result.length() == 0) + quit(_unexpected_eof, "Unexpected end of file or white-space - token expected"); +} + +std::string InStream::readToken() { + return readWord(); +} + +void InStream::readTokenTo(std::string &result) { + readWordTo(result); +} + +#ifdef __GNUC__ +__attribute__((const)) +#endif +static std::string __testlib_part(const std::string &s) { + std::string t; + for (size_t i = 0; i < s.length(); i++) + if (s[i] != '\0') + t += s[i]; + else + t += '~'; + if (t.length() <= 64) + return t; + else + return t.substr(0, 30) + "..." + t.substr(s.length() - 31, 31); +} + +#define __testlib_readMany(readMany, readOne, typeName, space) \ + if (size < 0) \ + quit(_fail, #readMany ": size should be non-negative."); \ + if (size > 100000000) \ + quit(_fail, #readMany ": size should be at most 100000000."); \ + \ + std::vector result(size); \ + readManyIteration = indexBase; \ + \ + for (int i = 0; i < size; i++) \ + { \ + result[i] = readOne; \ + readManyIteration++; \ + if (strict && space && i + 1 < size) \ + readSpace(); \ + } \ + \ + readManyIteration = NO_INDEX; \ + return result; \ + + +std::string InStream::readWord(const pattern &p, const std::string &variableName) { + readWordTo(_tmpReadToken); + if (!p.matches(_tmpReadToken)) { + if (readManyIteration == NO_INDEX) { + if (variableName.empty()) + quit(_wa, + ("Token \"" + __testlib_part(_tmpReadToken) + "\" doesn't correspond to pattern \"" + p.src() + + "\"").c_str()); + else + quit(_wa, ("Token parameter [name=" + variableName + "] equals to \"" + __testlib_part(_tmpReadToken) + + "\", doesn't correspond to pattern \"" + p.src() + "\"").c_str()); + } else { + if (variableName.empty()) + quit(_wa, ("Token element [index=" + vtos(readManyIteration) + "] equals to \"" + + __testlib_part(_tmpReadToken) + "\" doesn't correspond to pattern \"" + p.src() + + "\"").c_str()); + else + quit(_wa, ("Token element " + variableName + "[" + vtos(readManyIteration) + "] equals to \"" + + __testlib_part(_tmpReadToken) + "\", doesn't correspond to pattern \"" + p.src() + + "\"").c_str()); + } + } + if (strict && !variableName.empty()) + validator.addVariable(variableName); + return _tmpReadToken; +} + +std::vector +InStream::readWords(int size, const pattern &p, const std::string &variablesName, int indexBase) { + __testlib_readMany(readWords, readWord(p, variablesName), std::string, true); + if (strict && !variablesName.empty()) + validator.addVariable(variablesName); +} + +std::vector InStream::readWords(int size, int indexBase) { + __testlib_readMany(readWords, readWord(), std::string, true); +} + +std::string InStream::readWord(const std::string &ptrn, const std::string &variableName) { + return readWord(pattern(ptrn), variableName); +} + +std::vector +InStream::readWords(int size, const std::string &ptrn, const std::string &variablesName, int indexBase) { + pattern p(ptrn); + __testlib_readMany(readWords, readWord(p, variablesName), std::string, true); + if (strict && !variablesName.empty()) + validator.addVariable(variablesName); +} + +std::string InStream::readToken(const pattern &p, const std::string &variableName) { + return readWord(p, variableName); +} + +std::vector +InStream::readTokens(int size, const pattern &p, const std::string &variablesName, int indexBase) { + __testlib_readMany(readTokens, readToken(p, variablesName), std::string, true); + if (strict && !variablesName.empty()) + validator.addVariable(variablesName); +} + +std::vector InStream::readTokens(int size, int indexBase) { + __testlib_readMany(readTokens, readToken(), std::string, true); +} + +std::string InStream::readToken(const std::string &ptrn, const std::string &variableName) { + return readWord(ptrn, variableName); +} + +std::vector +InStream::readTokens(int size, const std::string &ptrn, const std::string &variablesName, int indexBase) { + pattern p(ptrn); + __testlib_readMany(readTokens, readWord(p, variablesName), std::string, true); + if (strict && !variablesName.empty()) + validator.addVariable(variablesName); +} + +void InStream::readWordTo(std::string &result, const pattern &p, const std::string &variableName) { + readWordTo(result); + if (!p.matches(result)) { + if (variableName.empty()) + quit(_wa, ("Token \"" + __testlib_part(result) + "\" doesn't correspond to pattern \"" + p.src() + + "\"").c_str()); + else + quit(_wa, ("Token parameter [name=" + variableName + "] equals to \"" + __testlib_part(result) + + "\", doesn't correspond to pattern \"" + p.src() + "\"").c_str()); + } + if (strict && !variableName.empty()) + validator.addVariable(variableName); +} + +void InStream::readWordTo(std::string &result, const std::string &ptrn, const std::string &variableName) { + return readWordTo(result, pattern(ptrn), variableName); +} + +void InStream::readTokenTo(std::string &result, const pattern &p, const std::string &variableName) { + return readWordTo(result, p, variableName); +} + +void InStream::readTokenTo(std::string &result, const std::string &ptrn, const std::string &variableName) { + return readWordTo(result, ptrn, variableName); +} + +#ifdef __GNUC__ +__attribute__((pure)) +#endif +static inline bool equals(long long integer, const char *s) { + if (integer == LLONG_MIN) + return strcmp(s, "-9223372036854775808") == 0; + + if (integer == 0LL) + return strcmp(s, "0") == 0; + + size_t length = strlen(s); + + if (length == 0) + return false; + + if (integer < 0 && s[0] != '-') + return false; + + if (integer < 0) + s++, length--, integer = -integer; + + if (length == 0) + return false; + + while (integer > 0) { + int digit = int(integer % 10); + + if (s[length - 1] != '0' + digit) + return false; + + length--; + integer /= 10; + } + + return length == 0; +} + +#ifdef __GNUC__ +__attribute__((pure)) +#endif +static inline bool equals(unsigned long long integer, const char *s) { + if (integer == ULLONG_MAX) + return strcmp(s, "18446744073709551615") == 0; + + if (integer == 0ULL) + return strcmp(s, "0") == 0; + + size_t length = strlen(s); + + if (length == 0) + return false; + + while (integer > 0) { + int digit = int(integer % 10); + + if (s[length - 1] != '0' + digit) + return false; + + length--; + integer /= 10; + } + + return length == 0; +} + +static inline double stringToDouble(InStream &in, const char *buffer) { + double result; + + size_t length = strlen(buffer); + + int minusCount = 0; + int plusCount = 0; + int decimalPointCount = 0; + int digitCount = 0; + int eCount = 0; + + for (size_t i = 0; i < length; i++) { + if (('0' <= buffer[i] && buffer[i] <= '9') || buffer[i] == '.' + || buffer[i] == 'e' || buffer[i] == 'E' + || buffer[i] == '-' || buffer[i] == '+') { + if ('0' <= buffer[i] && buffer[i] <= '9') + digitCount++; + if (buffer[i] == 'e' || buffer[i] == 'E') + eCount++; + if (buffer[i] == '-') + minusCount++; + if (buffer[i] == '+') + plusCount++; + if (buffer[i] == '.') + decimalPointCount++; + } else + in.quit(_pe, ("Expected double, but \"" + __testlib_part(buffer) + "\" found").c_str()); + } + + // If for sure is not a number in standard notation or in e-notation. + if (digitCount == 0 || minusCount > 2 || plusCount > 2 || decimalPointCount > 1 || eCount > 1) + in.quit(_pe, ("Expected double, but \"" + __testlib_part(buffer) + "\" found").c_str()); + + char *suffix = new char[length + 1]; + std::memset(suffix, 0, length + 1); + int scanned = std::sscanf(buffer, "%lf%s", &result, suffix); + bool empty = strlen(suffix) == 0; + delete[] suffix; + + if (scanned == 1 || (scanned == 2 && empty)) { + if (__testlib_isNaN(result)) + in.quit(_pe, ("Expected double, but \"" + __testlib_part(buffer) + "\" found").c_str()); + return result; + } else + in.quit(_pe, ("Expected double, but \"" + __testlib_part(buffer) + "\" found").c_str()); +} + +static inline double stringToDouble(InStream &in, const std::string& buffer) { + for (size_t i = 0; i < buffer.length(); i++) + if (buffer[i] == '\0') + in.quit(_pe, ("Expected double, but \"" + __testlib_part(buffer) + "\" found (it contains \\0)").c_str()); + return stringToDouble(in, buffer.c_str()); +} + +static inline double stringToStrictDouble(InStream &in, const char *buffer, + int minAfterPointDigitCount, int maxAfterPointDigitCount) { + if (minAfterPointDigitCount < 0) + in.quit(_fail, "stringToStrictDouble: minAfterPointDigitCount should be non-negative."); + + if (minAfterPointDigitCount > maxAfterPointDigitCount) + in.quit(_fail, + "stringToStrictDouble: minAfterPointDigitCount should be less or equal to maxAfterPointDigitCount."); + + double result; + + size_t length = strlen(buffer); + + if (length == 0 || length > 1000) + in.quit(_pe, ("Expected strict double, but \"" + __testlib_part(buffer) + "\" found").c_str()); + + if (buffer[0] != '-' && (buffer[0] < '0' || buffer[0] > '9')) + in.quit(_pe, ("Expected strict double, but \"" + __testlib_part(buffer) + "\" found").c_str()); + + int pointPos = -1; + for (size_t i = 1; i + 1 < length; i++) { + if (buffer[i] == '.') { + if (pointPos > -1) + in.quit(_pe, ("Expected strict double, but \"" + __testlib_part(buffer) + "\" found").c_str()); + pointPos = int(i); + } + if (buffer[i] != '.' && (buffer[i] < '0' || buffer[i] > '9')) + in.quit(_pe, ("Expected strict double, but \"" + __testlib_part(buffer) + "\" found").c_str()); + } + + if (buffer[length - 1] < '0' || buffer[length - 1] > '9') + in.quit(_pe, ("Expected strict double, but \"" + __testlib_part(buffer) + "\" found").c_str()); + + int afterDigitsCount = (pointPos == -1 ? 0 : int(length) - pointPos - 1); + if (afterDigitsCount < minAfterPointDigitCount || afterDigitsCount > maxAfterPointDigitCount) + in.quit(_pe, ("Expected strict double with number of digits after point in range [" + + vtos(minAfterPointDigitCount) + + "," + + vtos(maxAfterPointDigitCount) + + "], but \"" + __testlib_part(buffer) + "\" found").c_str() + ); + + int firstDigitPos = -1; + for (size_t i = 0; i < length; i++) + if (buffer[i] >= '0' && buffer[i] <= '9') { + firstDigitPos = int(i); + break; + } + + if (firstDigitPos > 1 || firstDigitPos == -1) + in.quit(_pe, ("Expected strict double, but \"" + __testlib_part(buffer) + "\" found").c_str()); + + if (buffer[firstDigitPos] == '0' && firstDigitPos + 1 < int(length) + && buffer[firstDigitPos + 1] >= '0' && buffer[firstDigitPos + 1] <= '9') + in.quit(_pe, ("Expected strict double, but \"" + __testlib_part(buffer) + "\" found").c_str()); + + char *suffix = new char[length + 1]; + std::memset(suffix, 0, length + 1); + int scanned = std::sscanf(buffer, "%lf%s", &result, suffix); + bool empty = strlen(suffix) == 0; + delete[] suffix; + + if (scanned == 1 || (scanned == 2 && empty)) { + if (__testlib_isNaN(result) || __testlib_isInfinite(result)) + in.quit(_pe, ("Expected double, but \"" + __testlib_part(buffer) + "\" found").c_str()); + if (buffer[0] == '-' && result >= 0) + in.quit(_pe, ("Redundant minus in \"" + __testlib_part(buffer) + "\" found").c_str()); + return result; + } else + in.quit(_pe, ("Expected double, but \"" + __testlib_part(buffer) + "\" found").c_str()); +} + +static inline double stringToStrictDouble(InStream &in, const std::string& buffer, + int minAfterPointDigitCount, int maxAfterPointDigitCount) { + for (size_t i = 0; i < buffer.length(); i++) + if (buffer[i] == '\0') + in.quit(_pe, ("Expected double, but \"" + __testlib_part(buffer) + "\" found (it contains \\0)").c_str()); + return stringToStrictDouble(in, buffer.c_str(), minAfterPointDigitCount, maxAfterPointDigitCount); +} + +static inline long long stringToLongLong(InStream &in, const char *buffer) { + size_t length = strlen(buffer); + if (length == 0 || length > 20) + in.quit(_pe, ("Expected integer, but \"" + __testlib_part(buffer) + "\" found").c_str()); + + bool has_minus = (length > 1 && buffer[0] == '-'); + int zeroes = 0; + bool processingZeroes = true; + + for (int i = (has_minus ? 1 : 0); i < int(length); i++) { + if (buffer[i] == '0' && processingZeroes) + zeroes++; + else + processingZeroes = false; + + if (buffer[i] < '0' || buffer[i] > '9') + in.quit(_pe, ("Expected integer, but \"" + __testlib_part(buffer) + "\" found").c_str()); + } + + long long int result; + try { + result = std::stoll(buffer); + } catch (const std::exception&) { + in.quit(_pe, ("Expected integer, but \"" + __testlib_part(buffer) + "\" found").c_str()); + } catch (...) { + in.quit(_pe, ("Expected integer, but \"" + __testlib_part(buffer) + "\" found").c_str()); + } + + if ((zeroes > 0 && (result != 0 || has_minus)) || zeroes > 1) + in.quit(_pe, ("Expected integer, but \"" + __testlib_part(buffer) + "\" found").c_str()); + + return result; +} + +static inline long long stringToLongLong(InStream &in, const std::string& buffer) { + for (size_t i = 0; i < buffer.length(); i++) + if (buffer[i] == '\0') + in.quit(_pe, ("Expected integer, but \"" + __testlib_part(buffer) + "\" found (it contains \\0)").c_str()); + return stringToLongLong(in, buffer.c_str()); +} + +static inline unsigned long long stringToUnsignedLongLong(InStream &in, const char *buffer) { + size_t length = strlen(buffer); + + if (length == 0 || length > 20) + in.quit(_pe, ("Expected unsigned integer, but \"" + __testlib_part(buffer) + "\" found").c_str()); + if (length > 1 && buffer[0] == '0') + in.quit(_pe, ("Expected unsigned integer, but \"" + __testlib_part(buffer) + "\" found").c_str()); + + for (int i = 0; i < int(length); i++) { + if (buffer[i] < '0' || buffer[i] > '9') + in.quit(_pe, ("Expected unsigned integer, but \"" + __testlib_part(buffer) + "\" found").c_str()); + } + + unsigned long long result; + try { + result = std::stoull(buffer); + } catch (const std::exception&) { + in.quit(_pe, ("Expected unsigned integer, but \"" + __testlib_part(buffer) + "\" found").c_str()); + } catch (...) { + in.quit(_pe, ("Expected unsigned integer, but \"" + __testlib_part(buffer) + "\" found").c_str()); + } + + return result; +} + +static inline long long stringToUnsignedLongLong(InStream &in, const std::string& buffer) { + for (size_t i = 0; i < buffer.length(); i++) + if (buffer[i] == '\0') + in.quit(_pe, ("Expected unsigned integer, but \"" + __testlib_part(buffer) + "\" found (it contains \\0)").c_str()); + return stringToUnsignedLongLong(in, buffer.c_str()); +} + +int InStream::readInteger() { + if (!strict && seekEof()) + quit(_unexpected_eof, "Unexpected end of file - int32 expected"); + + readWordTo(_tmpReadToken); + + long long value = stringToLongLong(*this, _tmpReadToken); + if (value < INT_MIN || value > INT_MAX) + quit(_pe, ("Expected int32, but \"" + __testlib_part(_tmpReadToken) + "\" found").c_str()); + + return int(value); +} + +long long InStream::readLong() { + if (!strict && seekEof()) + quit(_unexpected_eof, "Unexpected end of file - int64 expected"); + + readWordTo(_tmpReadToken); + + return stringToLongLong(*this, _tmpReadToken); +} + +unsigned long long InStream::readUnsignedLong() { + if (!strict && seekEof()) + quit(_unexpected_eof, "Unexpected end of file - int64 expected"); + + readWordTo(_tmpReadToken); + + return stringToUnsignedLongLong(*this, _tmpReadToken); +} + +long long InStream::readLong(long long minv, long long maxv, const std::string &variableName) { + long long result = readLong(); + + if (result < minv || result > maxv) { + if (readManyIteration == NO_INDEX) { + if (variableName.empty()) + quit(_wa, ("Integer " + vtos(result) + " violates the range [" + toHumanReadableString(minv) + ", " + toHumanReadableString(maxv) + + "]").c_str()); + else + quit(_wa, ("Integer parameter [name=" + std::string(variableName) + "] equals to " + vtos(result) + + ", violates the range [" + toHumanReadableString(minv) + ", " + toHumanReadableString(maxv) + "]").c_str()); + } else { + if (variableName.empty()) + quit(_wa, ("Integer element [index=" + vtos(readManyIteration) + "] equals to " + vtos(result) + + ", violates the range [" + toHumanReadableString(minv) + ", " + toHumanReadableString(maxv) + "]").c_str()); + else + quit(_wa, + ("Integer element " + std::string(variableName) + "[" + vtos(readManyIteration) + "] equals to " + + vtos(result) + ", violates the range [" + toHumanReadableString(minv) + ", " + toHumanReadableString(maxv) + "]").c_str()); + } + } + + if (strict && !variableName.empty()) { + validator.addBoundsHit(variableName, ValidatorBoundsHit(minv == result, maxv == result)); + validator.adjustConstantBounds(variableName, minv, maxv); + validator.addVariable(variableName); + } + + return result; +} + +std::vector +InStream::readLongs(int size, long long minv, long long maxv, const std::string &variablesName, int indexBase) { + __testlib_readMany(readLongs, readLong(minv, maxv, variablesName), long long, true) + if (strict && !variablesName.empty()) + validator.addVariable(variablesName); +} + +std::vector InStream::readLongs(int size, int indexBase) { + __testlib_readMany(readLongs, readLong(), long long, true) +} + +unsigned long long +InStream::readUnsignedLong(unsigned long long minv, unsigned long long maxv, const std::string &variableName) { + unsigned long long result = readUnsignedLong(); + + if (result < minv || result > maxv) { + if (readManyIteration == NO_INDEX) { + if (variableName.empty()) + quit(_wa, + ("Unsigned integer " + vtos(result) + " violates the range [" + toHumanReadableString(minv) + ", " + toHumanReadableString(maxv) + + "]").c_str()); + else + quit(_wa, + ("Unsigned integer parameter [name=" + std::string(variableName) + "] equals to " + vtos(result) + + ", violates the range [" + toHumanReadableString(minv) + ", " + toHumanReadableString(maxv) + "]").c_str()); + } else { + if (variableName.empty()) + quit(_wa, + ("Unsigned integer element [index=" + vtos(readManyIteration) + "] equals to " + vtos(result) + + ", violates the range [" + toHumanReadableString(minv) + ", " + toHumanReadableString(maxv) + "]").c_str()); + else + quit(_wa, ("Unsigned integer element " + std::string(variableName) + "[" + vtos(readManyIteration) + + "] equals to " + vtos(result) + ", violates the range [" + toHumanReadableString(minv) + ", " + toHumanReadableString(maxv) + + "]").c_str()); + } + } + + if (strict && !variableName.empty()) { + validator.addBoundsHit(variableName, ValidatorBoundsHit(minv == result, maxv == result)); + validator.adjustConstantBounds(variableName, minv, maxv); + validator.addVariable(variableName); + } + + return result; +} + +std::vector InStream::readUnsignedLongs(int size, unsigned long long minv, unsigned long long maxv, + const std::string &variablesName, int indexBase) { + __testlib_readMany(readUnsignedLongs, readUnsignedLong(minv, maxv, variablesName), unsigned long long, true) + if (strict && !variablesName.empty()) + validator.addVariable(variablesName); +} + +std::vector InStream::readUnsignedLongs(int size, int indexBase) { + __testlib_readMany(readUnsignedLongs, readUnsignedLong(), unsigned long long, true) +} + +unsigned long long +InStream::readLong(unsigned long long minv, unsigned long long maxv, const std::string &variableName) { + return readUnsignedLong(minv, maxv, variableName); +} + +int InStream::readInt() { + return readInteger(); +} + +int InStream::readInt(int minv, int maxv, const std::string &variableName) { + int result = readInt(); + + if (result < minv || result > maxv) { + if (readManyIteration == NO_INDEX) { + if (variableName.empty()) + quit(_wa, ("Integer " + vtos(result) + " violates the range [" + toHumanReadableString(minv) + ", " + toHumanReadableString(maxv) + + "]").c_str()); + else + quit(_wa, ("Integer parameter [name=" + std::string(variableName) + "] equals to " + vtos(result) + + ", violates the range [" + toHumanReadableString(minv) + ", " + toHumanReadableString(maxv) + "]").c_str()); + } else { + if (variableName.empty()) + quit(_wa, ("Integer element [index=" + vtos(readManyIteration) + "] equals to " + vtos(result) + + ", violates the range [" + toHumanReadableString(minv) + ", " + toHumanReadableString(maxv) + "]").c_str()); + else + quit(_wa, + ("Integer element " + std::string(variableName) + "[" + vtos(readManyIteration) + "] equals to " + + vtos(result) + ", violates the range [" + toHumanReadableString(minv) + ", " + toHumanReadableString(maxv) + "]").c_str()); + } + } + + if (strict && !variableName.empty()) { + validator.addBoundsHit(variableName, ValidatorBoundsHit(minv == result, maxv == result)); + validator.adjustConstantBounds(variableName, minv, maxv); + validator.addVariable(variableName); + } + + return result; +} + +int InStream::readInteger(int minv, int maxv, const std::string &variableName) { + return readInt(minv, maxv, variableName); +} + +std::vector InStream::readInts(int size, int minv, int maxv, const std::string &variablesName, int indexBase) { + __testlib_readMany(readInts, readInt(minv, maxv, variablesName), int, true) + if (strict && !variablesName.empty()) + validator.addVariable(variablesName); +} + +std::vector InStream::readInts(int size, int indexBase) { + __testlib_readMany(readInts, readInt(), int, true) +} + +std::vector InStream::readIntegers(int size, int minv, int maxv, const std::string &variablesName, int indexBase) { + __testlib_readMany(readIntegers, readInt(minv, maxv, variablesName), int, true) + if (strict && !variablesName.empty()) + validator.addVariable(variablesName); +} + +std::vector InStream::readIntegers(int size, int indexBase) { + __testlib_readMany(readIntegers, readInt(), int, true) +} + +double InStream::readReal() { + if (!strict && seekEof()) + quit(_unexpected_eof, "Unexpected end of file - double expected"); + + return stringToDouble(*this, readWord()); +} + +double InStream::readDouble() { + return readReal(); +} + +double InStream::readReal(double minv, double maxv, const std::string &variableName) { + double result = readReal(); + + if (result < minv || result > maxv) { + if (readManyIteration == NO_INDEX) { + if (variableName.empty()) + quit(_wa, ("Double " + vtos(result) + " violates the range [" + vtos(minv) + ", " + vtos(maxv) + + "]").c_str()); + else + quit(_wa, ("Double parameter [name=" + std::string(variableName) + "] equals to " + vtos(result) + + ", violates the range [" + vtos(minv) + ", " + vtos(maxv) + "]").c_str()); + } else { + if (variableName.empty()) + quit(_wa, ("Double element [index=" + vtos(readManyIteration) + "] equals to " + vtos(result) + + ", violates the range [" + vtos(minv) + ", " + vtos(maxv) + "]").c_str()); + else + quit(_wa, + ("Double element " + std::string(variableName) + "[" + vtos(readManyIteration) + "] equals to " + + vtos(result) + ", violates the range [" + vtos(minv) + ", " + vtos(maxv) + "]").c_str()); + } + } + + if (strict && !variableName.empty()) { + validator.addBoundsHit(variableName, ValidatorBoundsHit( + doubleDelta(minv, result) < ValidatorBoundsHit::EPS, + doubleDelta(maxv, result) < ValidatorBoundsHit::EPS + )); + validator.adjustConstantBounds(variableName, minv, maxv); + validator.addVariable(variableName); + } + + return result; +} + +std::vector +InStream::readReals(int size, double minv, double maxv, const std::string &variablesName, int indexBase) { + __testlib_readMany(readReals, readReal(minv, maxv, variablesName), double, true) + if (strict && !variablesName.empty()) + validator.addVariable(variablesName); +} + +std::vector InStream::readReals(int size, int indexBase) { + __testlib_readMany(readReals, readReal(), double, true) +} + +double InStream::readDouble(double minv, double maxv, const std::string &variableName) { + return readReal(minv, maxv, variableName); +} + +std::vector +InStream::readDoubles(int size, double minv, double maxv, const std::string &variablesName, int indexBase) { + __testlib_readMany(readDoubles, readDouble(minv, maxv, variablesName), double, true) + if (strict && !variablesName.empty()) + validator.addVariable(variablesName); +} + +std::vector InStream::readDoubles(int size, int indexBase) { + __testlib_readMany(readDoubles, readDouble(), double, true) +} + +double InStream::readStrictReal(double minv, double maxv, + int minAfterPointDigitCount, int maxAfterPointDigitCount, + const std::string &variableName) { + if (!strict && seekEof()) + quit(_unexpected_eof, "Unexpected end of file - strict double expected"); + + double result = stringToStrictDouble(*this, readWord(), minAfterPointDigitCount, maxAfterPointDigitCount); + + if (result < minv || result > maxv) { + if (readManyIteration == NO_INDEX) { + if (variableName.empty()) + quit(_wa, ("Strict double " + vtos(result) + " violates the range [" + vtos(minv) + ", " + vtos(maxv) + + "]").c_str()); + else + quit(_wa, + ("Strict double parameter [name=" + std::string(variableName) + "] equals to " + vtos(result) + + ", violates the range [" + vtos(minv) + ", " + vtos(maxv) + "]").c_str()); + } else { + if (variableName.empty()) + quit(_wa, ("Strict double element [index=" + vtos(readManyIteration) + "] equals to " + vtos(result) + + ", violates the range [" + vtos(minv) + ", " + vtos(maxv) + "]").c_str()); + else + quit(_wa, ("Strict double element " + std::string(variableName) + "[" + vtos(readManyIteration) + + "] equals to " + vtos(result) + ", violates the range [" + vtos(minv) + ", " + vtos(maxv) + + "]").c_str()); + } + } + + if (strict && !variableName.empty()) { + validator.addBoundsHit(variableName, ValidatorBoundsHit( + doubleDelta(minv, result) < ValidatorBoundsHit::EPS, + doubleDelta(maxv, result) < ValidatorBoundsHit::EPS + )); + validator.adjustConstantBounds(variableName, minv, maxv); + validator.addVariable(variableName); + } + + return result; +} + +std::vector InStream::readStrictReals(int size, double minv, double maxv, + int minAfterPointDigitCount, int maxAfterPointDigitCount, + const std::string &variablesName, int indexBase) { + __testlib_readMany(readStrictReals, + readStrictReal(minv, maxv, minAfterPointDigitCount, maxAfterPointDigitCount, variablesName), + double, true) + if (strict && !variablesName.empty()) + validator.addVariable(variablesName); +} + +double InStream::readStrictDouble(double minv, double maxv, + int minAfterPointDigitCount, int maxAfterPointDigitCount, + const std::string &variableName) { + return readStrictReal(minv, maxv, + minAfterPointDigitCount, maxAfterPointDigitCount, + variableName); +} + +std::vector InStream::readStrictDoubles(int size, double minv, double maxv, + int minAfterPointDigitCount, int maxAfterPointDigitCount, + const std::string &variablesName, int indexBase) { + __testlib_readMany(readStrictDoubles, + readStrictDouble(minv, maxv, minAfterPointDigitCount, maxAfterPointDigitCount, variablesName), + double, true) + if (strict && !variablesName.empty()) + validator.addVariable(variablesName); +} + +bool InStream::eof() { + if (!strict && NULL == reader) + return true; + + return reader->eof(); +} + +bool InStream::seekEof() { + if (!strict && NULL == reader) + return true; + skipBlanks(); + return eof(); +} + +bool InStream::eoln() { + if (!strict && NULL == reader) + return true; + + int c = reader->nextChar(); + + if (!strict) { + if (c == EOFC) + return true; + + if (c == CR) { + c = reader->nextChar(); + + if (c != LF) { + reader->unreadChar(c); + reader->unreadChar(CR); + return false; + } else + return true; + } + + if (c == LF) + return true; + + reader->unreadChar(c); + return false; + } else { + bool returnCr = false; + +#if (defined(ON_WINDOWS) && !defined(FOR_LINUX)) || defined(FOR_WINDOWS) + if (c != CR) { + reader->unreadChar(c); + return false; + } else { + if (!returnCr) + returnCr = true; + c = reader->nextChar(); + } +#endif + if (c != LF) { + reader->unreadChar(c); + if (returnCr) + reader->unreadChar(CR); + return false; + } + + return true; + } +} + +void InStream::readEoln() { + lastLine = reader->getLine(); + if (!eoln()) + quit(_pe, "Expected EOLN"); +} + +void InStream::readEof() { + lastLine = reader->getLine(); + if (!eof()) + quit(_pe, "Expected EOF"); + + if (TestlibFinalizeGuard::alive && this == &inf) + testlibFinalizeGuard.readEofCount++; +} + +bool InStream::seekEoln() { + if (!strict && NULL == reader) + return true; + + int cur; + do { + cur = reader->nextChar(); + } while (cur == SPACE || cur == TAB); + + reader->unreadChar(cur); + return eoln(); +} + +void InStream::nextLine() { + readLine(); +} + +void InStream::readStringTo(std::string &result) { + if (NULL == reader) + quit(_pe, "Expected line"); + + result.clear(); + + for (;;) { + int cur = reader->curChar(); + + if (cur == LF || cur == EOFC) + break; + + if (cur == CR) { + cur = reader->nextChar(); + if (reader->curChar() == LF) { + reader->unreadChar(cur); + break; + } + } + + lastLine = reader->getLine(); + result += char(reader->nextChar()); + } + + if (strict) + readEoln(); + else + eoln(); +} + +std::string InStream::readString() { + readStringTo(_tmpReadToken); + return _tmpReadToken; +} + +std::vector InStream::readStrings(int size, int indexBase) { + __testlib_readMany(readStrings, readString(), std::string, false) +} + +void InStream::readStringTo(std::string &result, const pattern &p, const std::string &variableName) { + readStringTo(result); + if (!p.matches(result)) { + if (readManyIteration == NO_INDEX) { + if (variableName.empty()) + quit(_wa, ("Line \"" + __testlib_part(result) + "\" doesn't correspond to pattern \"" + p.src() + + "\"").c_str()); + else + quit(_wa, ("Line [name=" + variableName + "] equals to \"" + __testlib_part(result) + + "\", doesn't correspond to pattern \"" + p.src() + "\"").c_str()); + } else { + if (variableName.empty()) + quit(_wa, + ("Line element [index=" + vtos(readManyIteration) + "] equals to \"" + __testlib_part(result) + + "\" doesn't correspond to pattern \"" + p.src() + "\"").c_str()); + else + quit(_wa, + ("Line element " + std::string(variableName) + "[" + vtos(readManyIteration) + "] equals to \"" + + __testlib_part(result) + "\", doesn't correspond to pattern \"" + p.src() + "\"").c_str()); + } + } + if (strict && !variableName.empty()) + validator.addVariable(variableName); +} + +void InStream::readStringTo(std::string &result, const std::string &ptrn, const std::string &variableName) { + readStringTo(result, pattern(ptrn), variableName); +} + +std::string InStream::readString(const pattern &p, const std::string &variableName) { + readStringTo(_tmpReadToken, p, variableName); + return _tmpReadToken; +} + +std::vector +InStream::readStrings(int size, const pattern &p, const std::string &variablesName, int indexBase) { + __testlib_readMany(readStrings, readString(p, variablesName), std::string, false) + if (strict && !variablesName.empty()) + validator.addVariable(variablesName); +} + +std::string InStream::readString(const std::string &ptrn, const std::string &variableName) { + readStringTo(_tmpReadToken, ptrn, variableName); + return _tmpReadToken; +} + +std::vector +InStream::readStrings(int size, const std::string &ptrn, const std::string &variablesName, int indexBase) { + pattern p(ptrn); + __testlib_readMany(readStrings, readString(p, variablesName), std::string, false) + if (strict && !variablesName.empty()) + validator.addVariable(variablesName); +} + +void InStream::readLineTo(std::string &result) { + readStringTo(result); +} + +std::string InStream::readLine() { + return readString(); +} + +std::vector InStream::readLines(int size, int indexBase) { + __testlib_readMany(readLines, readString(), std::string, false) +} + +void InStream::readLineTo(std::string &result, const pattern &p, const std::string &variableName) { + readStringTo(result, p, variableName); +} + +void InStream::readLineTo(std::string &result, const std::string &ptrn, const std::string &variableName) { + readStringTo(result, ptrn, variableName); +} + +std::string InStream::readLine(const pattern &p, const std::string &variableName) { + return readString(p, variableName); +} + +std::vector +InStream::readLines(int size, const pattern &p, const std::string &variablesName, int indexBase) { + __testlib_readMany(readLines, readString(p, variablesName), std::string, false) + if (strict && !variablesName.empty()) + validator.addVariable(variablesName); +} + +std::string InStream::readLine(const std::string &ptrn, const std::string &variableName) { + return readString(ptrn, variableName); +} + +std::vector +InStream::readLines(int size, const std::string &ptrn, const std::string &variablesName, int indexBase) { + pattern p(ptrn); + __testlib_readMany(readLines, readString(p, variablesName), std::string, false) + if (strict && !variablesName.empty()) + validator.addVariable(variablesName); +} + +#ifdef __GNUC__ +__attribute__ ((format (printf, 3, 4))) +#endif +void InStream::ensuref(bool cond, const char *format, ...) { + if (!cond) { + FMT_TO_RESULT(format, format, message); + this->__testlib_ensure(cond, message); + } +} + +void InStream::__testlib_ensure(bool cond, std::string message) { + if (!cond) + this->quit(_wa, message.c_str()); +} + +void InStream::close() { + if (NULL != reader) { + reader->close(); + delete reader; + reader = NULL; + } + + opened = false; +} + +NORETURN void quit(TResult result, const std::string &msg) { + ouf.quit(result, msg.c_str()); +} + +NORETURN void quit(TResult result, const char *msg) { + ouf.quit(result, msg); +} + +NORETURN void __testlib_quitp(double points, const char *message) { + __testlib_points = points; + std::string stringPoints = removeDoubleTrailingZeroes(format("%.10f", points)); + + std::string quitMessage; + if (NULL == message || 0 == strlen(message)) + quitMessage = stringPoints; + else + quitMessage = stringPoints + " " + message; + + quit(_points, quitMessage.c_str()); +} + +NORETURN void __testlib_quitp(int points, const char *message) { + __testlib_points = points; + std::string stringPoints = format("%d", points); + + std::string quitMessage; + if (NULL == message || 0 == strlen(message)) + quitMessage = stringPoints; + else + quitMessage = stringPoints + " " + message; + + quit(_points, quitMessage.c_str()); +} + +NORETURN void quitp(float points, const std::string &message = "") { + __testlib_quitp(double(points), message.c_str()); +} + +NORETURN void quitp(double points, const std::string &message = "") { + __testlib_quitp(points, message.c_str()); +} + +NORETURN void quitp(long double points, const std::string &message = "") { + __testlib_quitp(double(points), message.c_str()); +} + +NORETURN void quitp(int points, const std::string &message = "") { + __testlib_quitp(points, message.c_str()); +} + +NORETURN void quitpi(const std::string &points_info, const std::string &message = "") { + if (points_info.find(' ') != std::string::npos) + quit(_fail, "Parameter 'points_info' can't contain spaces"); + if (message.empty()) + quit(_points, ("points_info=" + points_info).c_str()); + else + quit(_points, ("points_info=" + points_info + " " + message).c_str()); +} + +template +#ifdef __GNUC__ +__attribute__ ((format (printf, 2, 3))) +#endif +NORETURN void quitp(F points, const char *format, ...) { + FMT_TO_RESULT(format, format, message); + quitp(points, message); +} + +#ifdef __GNUC__ +__attribute__ ((format (printf, 2, 3))) +#endif +NORETURN void quitf(TResult result, const char *format, ...) { + FMT_TO_RESULT(format, format, message); + quit(result, message); +} + +#ifdef __GNUC__ +__attribute__ ((format (printf, 3, 4))) +#endif +void quitif(bool condition, TResult result, const char *format, ...) { + if (condition) { + FMT_TO_RESULT(format, format, message); + quit(result, message); + } +} + +NORETURN void __testlib_help() { + InStream::textColor(InStream::LightCyan); + std::fprintf(stderr, "TESTLIB %s, https://github.com/MikeMirzayanov/testlib/ ", VERSION); + std::fprintf(stderr, "by Mike Mirzayanov, copyright(c) 2005-2020\n"); + std::fprintf(stderr, "Checker name: \"%s\"\n", checkerName.c_str()); + InStream::textColor(InStream::LightGray); + + std::fprintf(stderr, "\n"); + std::fprintf(stderr, "Latest features: \n"); + for (size_t i = 0; i < sizeof(latestFeatures) / sizeof(char *); i++) { + std::fprintf(stderr, "*) %s\n", latestFeatures[i]); + } + std::fprintf(stderr, "\n"); + + std::fprintf(stderr, "Program must be run with the following arguments: \n"); + std::fprintf(stderr, " [--testset testset] [--group group] [ [<-appes>]]\n\n"); + + __testlib_exitCode = FAIL_EXIT_CODE; + std::exit(FAIL_EXIT_CODE); +} + +static void __testlib_ensuresPreconditions() { + // testlib assumes: sizeof(int) = 4. + __TESTLIB_STATIC_ASSERT(sizeof(int) == 4); + + // testlib assumes: INT_MAX == 2147483647. + __TESTLIB_STATIC_ASSERT(INT_MAX == 2147483647); + + // testlib assumes: sizeof(long long) = 8. + __TESTLIB_STATIC_ASSERT(sizeof(long long) == 8); + + // testlib assumes: sizeof(double) = 8. + __TESTLIB_STATIC_ASSERT(sizeof(double) == 8); + + // testlib assumes: no -ffast-math. + if (!__testlib_isNaN(+__testlib_nan())) + quit(_fail, "Function __testlib_isNaN is not working correctly: possible reason is '-ffast-math'"); + if (!__testlib_isNaN(-__testlib_nan())) + quit(_fail, "Function __testlib_isNaN is not working correctly: possible reason is '-ffast-math'"); +} + +std::string __testlib_testset; + +std::string getTestset() { + return __testlib_testset; +} + +std::string __testlib_group; + +std::string getGroup() { + return __testlib_group; +} + +static void __testlib_set_testset_and_group(int argc, char* argv[]) { + for (int i = 1; i < argc; i++) { + if (!strcmp("--testset", argv[i])) { + if (i + 1 < argc && strlen(argv[i + 1]) > 0) + __testlib_testset = argv[++i]; + else + quit(_fail, std::string("Expected non-empty testset after --testset command line parameter")); + } else if (!strcmp("--group", argv[i])) { + if (i + 1 < argc) + __testlib_group = argv[++i]; + else + quit(_fail, std::string("Expected group after --group command line parameter")); + } + } +} + +void registerGen(int argc, char *argv[], int randomGeneratorVersion) { + if (randomGeneratorVersion < 0 || randomGeneratorVersion > 1) + quitf(_fail, "Random generator version is expected to be 0 or 1."); + random_t::version = randomGeneratorVersion; + + __testlib_ensuresPreconditions(); + TestlibFinalizeGuard::registered = true; + + testlibMode = _generator; + __testlib_set_binary(stdin); + rnd.setSeed(argc, argv); + +#if __cplusplus > 199711L || defined(_MSC_VER) + prepareOpts(argc, argv); +#endif +} + +#ifdef USE_RND_AS_BEFORE_087 +void registerGen(int argc, char* argv[]) +{ + registerGen(argc, argv, 0); +} +#else +#ifdef __GNUC__ +#if (__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 4)) +__attribute__ ((deprecated("Use registerGen(argc, argv, 0) or registerGen(argc, argv, 1)." +" The third parameter stands for the random generator version." +" If you are trying to compile old generator use macro -DUSE_RND_AS_BEFORE_087 or registerGen(argc, argv, 0)." +" Version 1 has been released on Spring, 2013. Use it to write new generators."))) +#else +__attribute__ ((deprecated)) +#endif +#endif +#ifdef _MSC_VER +__declspec(deprecated("Use registerGen(argc, argv, 0) or registerGen(argc, argv, 1)." + " The third parameter stands for the random generator version." + " If you are trying to compile old generator use macro -DUSE_RND_AS_BEFORE_087 or registerGen(argc, argv, 0)." + " Version 1 has been released on Spring, 2013. Use it to write new generators.")) +#endif +void registerGen(int argc, char *argv[]) { + std::fprintf(stderr, "Use registerGen(argc, argv, 0) or registerGen(argc, argv, 1)." + " The third parameter stands for the random generator version." + " If you are trying to compile old generator use macro -DUSE_RND_AS_BEFORE_087 or registerGen(argc, argv, 0)." + " Version 1 has been released on Spring, 2013. Use it to write new generators.\n\n"); + registerGen(argc, argv, 0); +} +#endif + +void setAppesModeEncoding(std::string appesModeEncoding) { + static const char* const ENCODINGS[] = {"ascii", "utf-7", "utf-8", "utf-16", "utf-16le", "utf-16be", "utf-32", "utf-32le", "utf-32be", "iso-8859-1", +"iso-8859-2", "iso-8859-3", "iso-8859-4", "iso-8859-5", "iso-8859-6", "iso-8859-7", "iso-8859-8", "iso-8859-9", "iso-8859-10", "iso-8859-11", +"iso-8859-13", "iso-8859-14", "iso-8859-15", "iso-8859-16", "windows-1250", "windows-1251", "windows-1252", "windows-1253", "windows-1254", "windows-1255", +"windows-1256", "windows-1257", "windows-1258", "gb2312", "gbk", "gb18030", "big5", "shift-jis", "euc-jp", "euc-kr", +"euc-cn", "euc-tw", "koi8-r", "koi8-u", "tis-620", "ibm437", "ibm850", "ibm852", "ibm855", "ibm857", +"ibm860", "ibm861", "ibm862", "ibm863", "ibm865", "ibm866", "ibm869", "macroman", "maccentraleurope", "maciceland", +"maccroatian", "macromania", "maccyrillic", "macukraine", "macgreek", "macturkish", "machebrew", "macarabic", "macthai", "hz-gb-2312", +"iso-2022-jp", "iso-2022-kr", "iso-2022-cn", "armscii-8", "tscii", "iscii", "viscii", "geostd8", "cp949", "cp874", +"cp1006", "cp775", "cp858", "cp737", "cp853", "cp856", "cp922", "cp1046", "cp1125", "cp1131", +"ptcp154", "koi8-t", "koi8-ru", "mulelao-1", "cp1133", "iso-ir-166", "tcvn", "iso-ir-14", "iso-ir-87", "iso-ir-159"}; + + appesModeEncoding = lowerCase(appesModeEncoding); + bool valid = false; + for (size_t i = 0; i < sizeof(ENCODINGS) / sizeof(ENCODINGS[0]); i++) + if (appesModeEncoding == ENCODINGS[i]) { + valid = true; + break; + } + if (!valid) + quit(_fail, "Unexpected encoding for setAppesModeEncoding(encoding)"); + ::appesModeEncoding = appesModeEncoding; +} + +void registerInteraction(int argc, char *argv[]) { + __testlib_ensuresPreconditions(); + __testlib_set_testset_and_group(argc, argv); + TestlibFinalizeGuard::registered = true; + + testlibMode = _interactor; + __testlib_set_binary(stdin); + + if (argc > 1 && !strcmp("--help", argv[1])) + __testlib_help(); + + if (argc < 3 || argc > 6) { + quit(_fail, std::string("Program must be run with the following arguments: ") + + std::string(" [ [ [<-appes>]]]") + + "\nUse \"--help\" to get help information"); + } + + if (argc <= 4) { + resultName = ""; + appesMode = false; + } + +#ifndef EJUDGE + if (argc == 5) { + resultName = argv[4]; + appesMode = false; + } + + if (argc == 6) { + if (strcmp("-APPES", argv[5]) && strcmp("-appes", argv[5])) { + quit(_fail, std::string("Program must be run with the following arguments: ") + + " [ [<-appes>]]"); + } else { + resultName = argv[4]; + appesMode = true; + } + } +#endif + + inf.init(argv[1], _input); + + tout.open(argv[2], std::ios_base::out); + if (tout.fail() || !tout.is_open()) + quit(_fail, std::string("Can not write to the test-output-file '") + argv[2] + std::string("'")); + + ouf.init(stdin, _output); + + if (argc >= 4) + ans.init(argv[3], _answer); + else + ans.name = "unopened answer stream"; +} + +void registerValidation() { + __testlib_ensuresPreconditions(); + TestlibFinalizeGuard::registered = true; + + testlibMode = _validator; + + __testlib_set_binary(stdin); + __testlib_set_binary(stdout); + __testlib_set_binary(stderr); + + inf.init(stdin, _input); + inf.strict = true; +} + +void registerValidation(int argc, char *argv[]) { + registerValidation(); + __testlib_set_testset_and_group(argc, argv); + + validator.initialize(); + TestlibFinalizeGuard::registered = true; + + std::string comment = "Validator must be run with the following arguments:" + " [--testset testset]" + " [--group group]" + " [--testOverviewLogFileName fileName]" + " [--testMarkupFileName fileName]" + " [--testCase testCase]" + " [--testCaseFileName fileName]" + ; + + for (int i = 1; i < argc; i++) { + if (!strcmp("--testset", argv[i])) { + if (i + 1 < argc && strlen(argv[i + 1]) > 0) + validator.setTestset(argv[++i]); + else + quit(_fail, comment); + } + if (!strcmp("--group", argv[i])) { + if (i + 1 < argc) + validator.setGroup(argv[++i]); + else + quit(_fail, comment); + } + if (!strcmp("--testOverviewLogFileName", argv[i])) { + if (i + 1 < argc) + validator.setTestOverviewLogFileName(argv[++i]); + else + quit(_fail, comment); + } + if (!strcmp("--testMarkupFileName", argv[i])) { + if (i + 1 < argc) + validator.setTestMarkupFileName(argv[++i]); + else + quit(_fail, comment); + } + if (!strcmp("--testCase", argv[i])) { + if (i + 1 < argc) { + long long testCase = stringToLongLong(inf, argv[++i]); + if (testCase < 1 || testCase >= __TESTLIB_MAX_TEST_CASE) + quit(_fail, format("Argument testCase should be between 1 and %d, but ", __TESTLIB_MAX_TEST_CASE) + + toString(testCase) + " found"); + validator.setTestCase(int(testCase)); + } else + quit(_fail, comment); + } + if (!strcmp("--testCaseFileName", argv[i])) { + if (i + 1 < argc) { + validator.setTestCaseFileName(argv[++i]); + } else + quit(_fail, comment); + } + } +} + +void addFeature(const std::string &feature) { + if (testlibMode != _validator) + quit(_fail, "Features are supported in validators only."); + validator.addFeature(feature); +} + +void feature(const std::string &feature) { + if (testlibMode != _validator) + quit(_fail, "Features are supported in validators only."); + validator.feature(feature); +} + +class Checker { +private: + bool _initialized; + std::string _testset; + std::string _group; + +public: + Checker() : _initialized(false), _testset("tests"), _group() { + } + + void initialize() { + _initialized = true; + } + + std::string testset() const { + if (!_initialized) + __testlib_fail("Checker should be initialized with registerTestlibCmd(argc, argv) instead of registerTestlibCmd() to support checker.testset()"); + return _testset; + } + + std::string group() const { + if (!_initialized) + __testlib_fail("Checker should be initialized with registerTestlibCmd(argc, argv) instead of registerTestlibCmd() to support checker.group()"); + return _group; + } + + void setTestset(const char *const testset) { + _testset = testset; + } + + void setGroup(const char *const group) { + _group = group; + } +} checker; + +void registerTestlibCmd(int argc, char *argv[]) { + __testlib_ensuresPreconditions(); + __testlib_set_testset_and_group(argc, argv); + TestlibFinalizeGuard::registered = true; + + testlibMode = _checker; + __testlib_set_binary(stdin); + + std::vector args(1, argv[0]); + checker.initialize(); + + for (int i = 1; i < argc; i++) { + if (!strcmp("--testset", argv[i])) { + if (i + 1 < argc && strlen(argv[i + 1]) > 0) + checker.setTestset(argv[++i]); + else + quit(_fail, std::string("Expected testset after --testset command line parameter")); + } else if (!strcmp("--group", argv[i])) { + if (i + 1 < argc) + checker.setGroup(argv[++i]); + else + quit(_fail, std::string("Expected group after --group command line parameter")); + } else + args.push_back(argv[i]); + } + + argc = int(args.size()); + if (argc > 1 && "--help" == args[1]) + __testlib_help(); + + if (argc < 4 || argc > 6) { + quit(_fail, std::string("Program must be run with the following arguments: ") + + std::string("[--testset testset] [--group group] [ [<-appes>]]") + + "\nUse \"--help\" to get help information"); + } + + if (argc == 4) { + resultName = ""; + appesMode = false; + } + +#ifndef EJUDGE + if (argc == 5) { + resultName = args[4]; + appesMode = false; + } + + if (argc == 6) { + if ("-APPES" != args[5] && "-appes" != args[5]) { + quit(_fail, std::string("Program must be run with the following arguments: ") + + " [ [<-appes>]]"); + } else { + resultName = args[4]; + appesMode = true; + } + } +#endif + + inf.init(args[1], _input); + ouf.init(args[2], _output); + ouf.skipBom(); + ans.init(args[3], _answer); +} + +void registerTestlib(int argc, ...) { + if (argc < 3 || argc > 5) + quit(_fail, std::string("Program must be run with the following arguments: ") + + " [ [<-appes>]]"); + + char **argv = new char *[argc + 1]; + + va_list ap; + va_start(ap, argc); + argv[0] = NULL; + for (int i = 0; i < argc; i++) { + argv[i + 1] = va_arg(ap, char*); + } + va_end(ap); + + registerTestlibCmd(argc + 1, argv); + delete[] argv; +} + +static inline void __testlib_ensure(bool cond, const std::string &msg) { + if (!cond) + quit(_fail, msg.c_str()); +} + +#ifdef __GNUC__ +__attribute__((unused)) +#endif +static inline void __testlib_ensure(bool cond, const char *msg) { + if (!cond) + quit(_fail, msg); +} + +#define ensure(cond) __testlib_ensure(cond, "Condition failed: \"" #cond "\"") +#define STRINGIZE_DETAIL(x) #x +#define STRINGIZE(x) STRINGIZE_DETAIL(x) +#define ensure_ext(cond) __testlib_ensure(cond, "Line " STRINGIZE(__LINE__) ": Condition failed: \"" #cond "\"") + +#ifdef __GNUC__ +__attribute__ ((format (printf, 2, 3))) +#endif +inline void ensuref(bool cond, const char *format, ...) { + if (!cond) { + FMT_TO_RESULT(format, format, message); + __testlib_ensure(cond, message); + } +} + +NORETURN static void __testlib_fail(const std::string &message) { + quitf(_fail, "%s", message.c_str()); +} + +#ifdef __GNUC__ +__attribute__ ((format (printf, 1, 2))) +#endif +void setName(const char *format, ...) { + FMT_TO_RESULT(format, format, name); + checkerName = name; +} + +/* + * Do not use random_shuffle, because it will produce different result + * for different C++ compilers. + * + * This implementation uses testlib random_t to produce random numbers, so + * it is stable. + */ +template +void shuffle(_RandomAccessIter __first, _RandomAccessIter __last) { + if (__first == __last) return; + for (_RandomAccessIter __i = __first + 1; __i != __last; ++__i) + std::iter_swap(__i, __first + rnd.next(int(__i - __first) + 1)); +} + + +template +#if defined(__GNUC__) && !defined(__clang__) +__attribute__ ((error("Don't use random_shuffle(), use shuffle() instead"))) +#endif +void random_shuffle(_RandomAccessIter, _RandomAccessIter) { + quitf(_fail, "Don't use random_shuffle(), use shuffle() instead"); +} + +#ifdef __GLIBC__ +# define RAND_THROW_STATEMENT throw() +#else +# define RAND_THROW_STATEMENT +#endif + +#if defined(__GNUC__) && !defined(__clang__) + +__attribute__ ((error("Don't use rand(), use rnd.next() instead"))) +#endif +#ifdef _MSC_VER +# pragma warning( disable : 4273 ) +#endif +int rand() RAND_THROW_STATEMENT +{ + quitf(_fail, "Don't use rand(), use rnd.next() instead"); + + /* This line never runs. */ + //throw "Don't use rand(), use rnd.next() instead"; +} + +#if defined(__GNUC__) && !defined(__clang__) + +__attribute__ ((error("Don't use srand(), you should use " +"'registerGen(argc, argv, 1);' to initialize generator seed " +"by hash code of the command line params. The third parameter " +"is randomGeneratorVersion (currently the latest is 1)."))) +#endif +#ifdef _MSC_VER +# pragma warning( disable : 4273 ) +#endif +void srand(unsigned int seed) RAND_THROW_STATEMENT +{ + quitf(_fail, "Don't use srand(), you should use " + "'registerGen(argc, argv, 1);' to initialize generator seed " + "by hash code of the command line params. The third parameter " + "is randomGeneratorVersion (currently the latest is 1) [ignored seed=%u].", seed); +} + +void startTest(int test) { + const std::string testFileName = vtos(test); + if (NULL == freopen(testFileName.c_str(), "wt", stdout)) + __testlib_fail("Unable to write file '" + testFileName + "'"); +} + +#ifdef __GNUC__ +__attribute__((const)) +#endif +inline std::string compress(const std::string &s) { + return __testlib_part(s); +} + +#ifdef __GNUC__ +__attribute__((const)) +#endif +inline std::string englishEnding(int x) { + x %= 100; + if (x / 10 == 1) + return "th"; + if (x % 10 == 1) + return "st"; + if (x % 10 == 2) + return "nd"; + if (x % 10 == 3) + return "rd"; + return "th"; +} + +template +#ifdef __GNUC__ +__attribute__((const)) +#endif +std::string join(_ForwardIterator first, _ForwardIterator last, _Separator separator) { + std::stringstream ss; + bool repeated = false; + for (_ForwardIterator i = first; i != last; i++) { + if (repeated) + ss << separator; + else + repeated = true; + ss << *i; + } + return ss.str(); +} + +template +#ifdef __GNUC__ +__attribute__((const)) +#endif +std::string join(_ForwardIterator first, _ForwardIterator last) { + return join(first, last, ' '); +} + +template +#ifdef __GNUC__ +__attribute__((const)) +#endif +std::string join(const _Collection &collection, _Separator separator) { + return join(collection.begin(), collection.end(), separator); +} + +template +#ifdef __GNUC__ +__attribute__((const)) +#endif +std::string join(const _Collection &collection) { + return join(collection, ' '); +} + +/** + * Splits string s by character separator returning exactly k+1 items, + * where k is the number of separator occurrences. + */ +#ifdef __GNUC__ +__attribute__((const)) +#endif +std::vector split(const std::string &s, char separator) { + std::vector result; + std::string item; + for (size_t i = 0; i < s.length(); i++) + if (s[i] == separator) { + result.push_back(item); + item = ""; + } else + item += s[i]; + result.push_back(item); + return result; +} + +/** + * Splits string s by character separators returning exactly k+1 items, + * where k is the number of separator occurrences. + */ +#ifdef __GNUC__ +__attribute__((const)) +#endif +std::vector split(const std::string &s, const std::string &separators) { + if (separators.empty()) + return std::vector(1, s); + + std::vector isSeparator(256); + for (size_t i = 0; i < separators.size(); i++) + isSeparator[(unsigned char) (separators[i])] = true; + + std::vector result; + std::string item; + for (size_t i = 0; i < s.length(); i++) + if (isSeparator[(unsigned char) (s[i])]) { + result.push_back(item); + item = ""; + } else + item += s[i]; + result.push_back(item); + return result; +} + +/** + * Splits string s by character separator returning non-empty items. + */ +#ifdef __GNUC__ +__attribute__((const)) +#endif +std::vector tokenize(const std::string &s, char separator) { + std::vector result; + std::string item; + for (size_t i = 0; i < s.length(); i++) + if (s[i] == separator) { + if (!item.empty()) + result.push_back(item); + item = ""; + } else + item += s[i]; + if (!item.empty()) + result.push_back(item); + return result; +} + +/** + * Splits string s by character separators returning non-empty items. + */ +#ifdef __GNUC__ +__attribute__((const)) +#endif +std::vector tokenize(const std::string &s, const std::string &separators) { + if (separators.empty()) + return std::vector(1, s); + + std::vector isSeparator(256); + for (size_t i = 0; i < separators.size(); i++) + isSeparator[(unsigned char) (separators[i])] = true; + + std::vector result; + std::string item; + for (size_t i = 0; i < s.length(); i++) + if (isSeparator[(unsigned char) (s[i])]) { + if (!item.empty()) + result.push_back(item); + item = ""; + } else + item += s[i]; + + if (!item.empty()) + result.push_back(item); + + return result; +} + +NORETURN void __testlib_expectedButFound(TResult result, std::string expected, std::string found, const char *prepend) { + std::string message; + if (strlen(prepend) != 0) + message = format("%s: expected '%s', but found '%s'", + compress(prepend).c_str(), compress(expected).c_str(), compress(found).c_str()); + else + message = format("expected '%s', but found '%s'", + compress(expected).c_str(), compress(found).c_str()); + quit(result, message); +} + +NORETURN void __testlib_expectedButFound(TResult result, double expected, double found, const char *prepend) { + std::string expectedString = removeDoubleTrailingZeroes(format("%.12f", expected)); + std::string foundString = removeDoubleTrailingZeroes(format("%.12f", found)); + __testlib_expectedButFound(result, expectedString, foundString, prepend); +} + +template +#ifdef __GNUC__ +__attribute__ ((format (printf, 4, 5))) +#endif +NORETURN void expectedButFound(TResult result, T expected, T found, const char *prependFormat = "", ...) { + FMT_TO_RESULT(prependFormat, prependFormat, prepend); + std::string expectedString = vtos(expected); + std::string foundString = vtos(found); + __testlib_expectedButFound(result, expectedString, foundString, prepend.c_str()); +} + +template<> +#ifdef __GNUC__ +__attribute__ ((format (printf, 4, 5))) +#endif +NORETURN void +expectedButFound(TResult result, std::string expected, std::string found, const char *prependFormat, ...) { + FMT_TO_RESULT(prependFormat, prependFormat, prepend); + __testlib_expectedButFound(result, expected, found, prepend.c_str()); +} + +template<> +#ifdef __GNUC__ +__attribute__ ((format (printf, 4, 5))) +#endif +NORETURN void expectedButFound(TResult result, double expected, double found, const char *prependFormat, ...) { + FMT_TO_RESULT(prependFormat, prependFormat, prepend); + std::string expectedString = removeDoubleTrailingZeroes(format("%.12f", expected)); + std::string foundString = removeDoubleTrailingZeroes(format("%.12f", found)); + __testlib_expectedButFound(result, expectedString, foundString, prepend.c_str()); +} + +template<> +#ifdef __GNUC__ +__attribute__ ((format (printf, 4, 5))) +#endif +NORETURN void +expectedButFound(TResult result, const char *expected, const char *found, const char *prependFormat, + ...) { + FMT_TO_RESULT(prependFormat, prependFormat, prepend); + __testlib_expectedButFound(result, std::string(expected), std::string(found), prepend.c_str()); +} + +template<> +#ifdef __GNUC__ +__attribute__ ((format (printf, 4, 5))) +#endif +NORETURN void expectedButFound(TResult result, float expected, float found, const char *prependFormat, ...) { + FMT_TO_RESULT(prependFormat, prependFormat, prepend); + __testlib_expectedButFound(result, double(expected), double(found), prepend.c_str()); +} + +template<> +#ifdef __GNUC__ +__attribute__ ((format (printf, 4, 5))) +#endif +NORETURN void +expectedButFound(TResult result, long double expected, long double found, const char *prependFormat, ...) { + FMT_TO_RESULT(prependFormat, prependFormat, prepend); + __testlib_expectedButFound(result, double(expected), double(found), prepend.c_str()); +} + +#if __cplusplus > 199711L || defined(_MSC_VER) +template +struct is_iterable { + template + static char test(typename U::iterator *x); + + template + static long test(U *x); + + static const bool value = sizeof(test(0)) == 1; +}; + +template +struct __testlib_enable_if { +}; + +template +struct __testlib_enable_if { + typedef T type; +}; + +template +typename __testlib_enable_if::value, void>::type __testlib_print_one(const T &t) { + std::cout << t; +} + +template +typename __testlib_enable_if::value, void>::type __testlib_print_one(const T &t) { + bool first = true; + for (typename T::const_iterator i = t.begin(); i != t.end(); i++) { + if (first) + first = false; + else + std::cout << " "; + std::cout << *i; + } +} + +template<> +typename __testlib_enable_if::value, void>::type +__testlib_print_one(const std::string &t) { + std::cout << t; +} + +template +void __println_range(A begin, B end) { + bool first = true; + for (B i = B(begin); i != end; i++) { + if (first) + first = false; + else + std::cout << " "; + __testlib_print_one(*i); + } + std::cout << std::endl; +} + +template +struct is_iterator { + static T makeT(); + + typedef void *twoptrs[2]; + + static twoptrs &test(...); + + template + static typename R::iterator_category *test(R); + + template + static void *test(R *); + + static const bool value = sizeof(test(makeT())) == sizeof(void *); +}; + +template +struct is_iterator::value>::type> { + static const bool value = false; +}; + +template +typename __testlib_enable_if::value, void>::type println(const A &a, const B &b) { + __testlib_print_one(a); + std::cout << " "; + __testlib_print_one(b); + std::cout << std::endl; +} + +template +typename __testlib_enable_if::value, void>::type println(const A &a, const B &b) { + __println_range(a, b); +} + +template +void println(const A *a, const A *b) { + __println_range(a, b); +} + +template<> +void println(const char *a, const char *b) { + __testlib_print_one(a); + std::cout << " "; + __testlib_print_one(b); + std::cout << std::endl; +} + +template +void println(const T &x) { + __testlib_print_one(x); + std::cout << std::endl; +} + +template +void println(const A &a, const B &b, const C &c) { + __testlib_print_one(a); + std::cout << " "; + __testlib_print_one(b); + std::cout << " "; + __testlib_print_one(c); + std::cout << std::endl; +} + +template +void println(const A &a, const B &b, const C &c, const D &d) { + __testlib_print_one(a); + std::cout << " "; + __testlib_print_one(b); + std::cout << " "; + __testlib_print_one(c); + std::cout << " "; + __testlib_print_one(d); + std::cout << std::endl; +} + +template +void println(const A &a, const B &b, const C &c, const D &d, const E &e) { + __testlib_print_one(a); + std::cout << " "; + __testlib_print_one(b); + std::cout << " "; + __testlib_print_one(c); + std::cout << " "; + __testlib_print_one(d); + std::cout << " "; + __testlib_print_one(e); + std::cout << std::endl; +} + +template +void println(const A &a, const B &b, const C &c, const D &d, const E &e, const F &f) { + __testlib_print_one(a); + std::cout << " "; + __testlib_print_one(b); + std::cout << " "; + __testlib_print_one(c); + std::cout << " "; + __testlib_print_one(d); + std::cout << " "; + __testlib_print_one(e); + std::cout << " "; + __testlib_print_one(f); + std::cout << std::endl; +} + +template +void println(const A &a, const B &b, const C &c, const D &d, const E &e, const F &f, const G &g) { + __testlib_print_one(a); + std::cout << " "; + __testlib_print_one(b); + std::cout << " "; + __testlib_print_one(c); + std::cout << " "; + __testlib_print_one(d); + std::cout << " "; + __testlib_print_one(e); + std::cout << " "; + __testlib_print_one(f); + std::cout << " "; + __testlib_print_one(g); + std::cout << std::endl; +} + +/* opts */ + +/** + * A struct for a singular testlib opt, containing the raw string value, + * and a boolean value for marking whether the opt is used. + */ +struct TestlibOpt { + std::string value; + bool used; + + TestlibOpt() : value(), used(false) {} +}; + +/** + * Get the type of opt based on the number of `-` at the beginning and the + * _validity_ of the key name. + * + * A valid key name must start with an alphabetical character. + * + * Returns: 1 if s has one `-` at the beginning, that is, "-keyName". + * 2 if s has two `-` at the beginning, that is, "--keyName". + * 0 otherwise. That is, if s has no `-` at the beginning, or has more + * than 2 at the beginning ("---keyName", "----keyName", ...), or the + * keyName is invalid (the first character is not an alphabetical + * character). + */ +size_t getOptType(char *s) { + if (!s || strlen(s) <= 1) + return 0; + + if (s[0] == '-') { + if (isalpha(s[1])) + return 1; + else if (s[1] == '-') + return isalpha(s[2]) ? 2 : 0; + } + + return 0; +} + +/** + * Parse the opt at a given index, and put it into the opts maps. + * + * An opt can has the following form: + * 1) -keyName=value or --keyName=value (ex. -n=10 --test-count=20) + * 2) -keyName value or --keyName value (ex. -n 10 --test-count 20) + * 3) -kNumval or --kNumval (ex. -n10 --t20) + * 4) -boolProperty or --boolProperty (ex. -sorted --tree-only) + * + * Only the second form consumes 2 arguments. The other consumes only 1 + * argument. + * + * In the third form, the key is a single character, and after the key is the + * value. The value _should_ be a number. + * + * In the forth form, the value is true. + * + * Params: + * - argc and argv: the number of command line arguments and the command line + * arguments themselves. + * - index: the starting index of the opts. + * - opts: the map containing the resulting opt. + * + * Returns: the number of consumed arguments to parse the opt. + * 0 if there is no arguments to parse. + * + * Algorithm details: + * TODO. Please refer to the implementation to see how the code handles the 3rd and 4th forms separately. + */ +size_t parseOpt(size_t argc, char *argv[], size_t index, std::map &opts) { + if (index >= argc) + return 0; + + size_t type = getOptType(argv[index]), inc = 1; + if (type > 0) { + std::string key(argv[index] + type), val; + size_t sep = key.find('='); + if (sep != std::string::npos) { + val = key.substr(sep + 1); + key = key.substr(0, sep); + } else { + if (index + 1 < argc && getOptType(argv[index + 1]) == 0) { + val = argv[index + 1]; + inc = 2; + } else { + if (key.length() > 1 && isdigit(key[1])) { + val = key.substr(1); + key = key.substr(0, 1); + } else { + val = "true"; + } + } + } + opts[key].value = val; + } else { + return inc; + } + + return inc; +} + +/** + * Global list containing all the arguments in the order given in the command line. + */ +std::vector __testlib_argv; + +/** + * Global dictionary containing all the parsed opts. + */ +std::map __testlib_opts; + +/** + * Whether automatic no unused opts ensurement should be done. This flag will + * be turned on when `has_opt` or `opt(key, default_value)` is called. + * + * The automatic ensurement can be suppressed when + * __testlib_ensureNoUnusedOptsSuppressed is true. + */ +bool __testlib_ensureNoUnusedOptsFlag = false; + +/** + * Suppress no unused opts automatic ensurement. Can be set to true with + * `suppressEnsureNoUnusedOpts()`. + */ +bool __testlib_ensureNoUnusedOptsSuppressed = false; + +/** + * Parse command line arguments into opts. + * The results are stored into __testlib_argv and __testlib_opts. + */ +void prepareOpts(int argc, char *argv[]) { + if (argc <= 0) + __testlib_fail("Opts: expected argc>=0 but found " + toString(argc)); + size_t n = static_cast(argc); // NOLINT(hicpp-use-auto,modernize-use-auto) + __testlib_opts = std::map(); + for (size_t index = 1; index < n; index += parseOpt(n, argv, index, __testlib_opts)); + __testlib_argv = std::vector(n); + for (size_t index = 0; index < n; index++) + __testlib_argv[index] = argv[index]; +} + +/** + * An utility function to get the argument with a given index. This function + * also print a readable message when no arguments are found. + */ +std::string __testlib_indexToArgv(int index) { + if (index < 0 || index >= int(__testlib_argv.size())) + __testlib_fail("Opts: index '" + toString(index) + "' is out of range [0," + + toString(__testlib_argv.size()) + ")"); + return __testlib_argv[size_t(index)]; +} + +/** + * An utility function to get the opt with a given key . This function + * also print a readable message when no opts are found. + */ +std::string __testlib_keyToOpts(const std::string &key) { + auto it = __testlib_opts.find(key); + if (it == __testlib_opts.end()) + __testlib_fail("Opts: unknown key '" + compress(key) + "'"); + it->second.used = true; + return it->second.value; +} + +template +T optValueToIntegral(const std::string &s, bool nonnegative); + +long double optValueToLongDouble(const std::string &s); + +std::string parseExponentialOptValue(const std::string &s) { + size_t pos = std::string::npos; + for (size_t i = 0; i < s.length(); i++) + if (s[i] == 'e' || s[i] == 'E') { + if (pos != std::string::npos) + __testlib_fail("Opts: expected typical exponential notation but '" + compress(s) + "' found"); + pos = i; + } + if (pos == std::string::npos) + return s; + std::string e = s.substr(pos + 1); + if (!e.empty() && e[0] == '+') + e = e.substr(1); + if (e.empty()) + __testlib_fail("Opts: expected typical exponential notation but '" + compress(s) + "' found"); + if (e.length() > 20) + __testlib_fail("Opts: expected typical exponential notation but '" + compress(s) + "' found"); + int ne = optValueToIntegral(e, false); + std::string num = s.substr(0, pos); + if (num.length() > 20) + __testlib_fail("Opts: expected typical exponential notation but '" + compress(s) + "' found"); + if (!num.empty() && num[0] == '+') + num = num.substr(1); + optValueToLongDouble(num); + bool minus = false; + if (num[0] == '-') { + minus = true; + num = num.substr(1); + } + for (int i = 0; i < +ne; i++) { + size_t sep = num.find('.'); + if (sep == std::string::npos) + num += '0'; + else { + if (sep + 1 == num.length()) + num[sep] = '0'; + else + std::swap(num[sep], num[sep + 1]); + } + } + for (int i = 0; i < -ne; i++) { + size_t sep = num.find('.'); + if (sep == std::string::npos) + num.insert(num.begin() + int(num.length()) - 1, '.'); + else { + if (sep == 0) + num.insert(num.begin() + 1, '0'); + else + std::swap(num[sep - 1], num[sep]); + } + } + while (!num.empty() && num[0] == '0') + num = num.substr(1); + while (num.find('.') != std::string::npos && num.back() == '0') + num = num.substr(0, num.length() - 1); + if (!num.empty() && num.back() == '.') + num = num.substr(0, num.length() - 1); + if ((!num.empty() && num[0] == '.') || num.empty()) + num.insert(num.begin(), '0'); + return (minus ? "-" : "") + num; +} + +template +T optValueToIntegral(const std::string &s_, bool nonnegative) { + std::string s(parseExponentialOptValue(s_)); + if (s.empty()) + __testlib_fail("Opts: expected integer but '" + compress(s_) + "' found"); + T value = 0; + long double about = 0.0; + signed char sign = +1; + size_t pos = 0; + if (s[pos] == '-') { + if (nonnegative) + __testlib_fail("Opts: expected non-negative integer but '" + compress(s_) + "' found"); + sign = -1; + pos++; + } + for (size_t i = pos; i < s.length(); i++) { + if (s[i] < '0' || s[i] > '9') + __testlib_fail("Opts: expected integer but '" + compress(s_) + "' found"); + value = T(value * 10 + s[i] - '0'); + about = about * 10 + s[i] - '0'; + } + value *= sign; + about *= sign; + if (fabsl(value - about) > 0.1) + __testlib_fail("Opts: integer overflow: expected integer but '" + compress(s_) + "' found"); + return value; +} + +long double optValueToLongDouble(const std::string &s_) { + std::string s(parseExponentialOptValue(s_)); + if (s.empty()) + __testlib_fail("Opts: expected float number but '" + compress(s_) + "' found"); + long double value = 0.0; + signed char sign = +1; + size_t pos = 0; + if (s[pos] == '-') { + sign = -1; + pos++; + } + bool period = false; + long double mul = 1.0; + for (size_t i = pos; i < s.length(); i++) { + if (s[i] == '.') { + if (period) + __testlib_fail("Opts: expected float number but '" + compress(s_) + "' found"); + else { + period = true; + continue; + } + } + if (period) + mul *= 10.0; + if (s[i] < '0' || s[i] > '9') + __testlib_fail("Opts: expected float number but '" + compress(s_) + "' found"); + if (period) + value += (s[i] - '0') / mul; + else + value = value * 10 + s[i] - '0'; + } + value *= sign; + return value; +} + +/** + * Return true if there is an opt with a given key. + * + * By calling this function, automatic ensurement for no unused opts will be + * done when the program is finalized. Call suppressEnsureNoUnusedOpts() to + * turn it off. + */ +bool has_opt(const std::string &key) { + __testlib_ensureNoUnusedOptsFlag = true; + return __testlib_opts.count(key) != 0; +} + +/* About the following part for opt with 2 and 3 arguments. + * + * To parse the argv/opts correctly for a give type (integer, floating point or + * string), some meta programming must be done to determine the type of + * the type, and use the correct parsing function accordingly. + * + * The pseudo algorithm for determining the type of T and parse it accordingly + * is as follows: + * + * if (T is integral type) { + * if (T is unsigned) { + * parse the argv/opt as an **unsigned integer** of type T. + * } else { + * parse the argv/opt as an **signed integer** of type T. + * } else { + * if (T is floating point type) { + * parse the argv/opt as an **floating point** of type T. + * } else { + * // T should be std::string + * just the raw content of the argv/opts. + * } + * } + * + * To help with meta programming, some `opt` function with 2 or 3 arguments are + * defined. + * + * Opt with 3 arguments: T opt(true/false is_integral, true/false is_unsigned, index/key) + * + * + The first argument is for determining whether the type T is an integral + * type. That is, the result of std::is_integral() should be passed to + * this argument. When false, the type _should_ be either floating point or a + * std::string. + * + * + The second argument is for determining whether the signedness of the type + * T (if it is unsigned or signed). That is, the result of + * std::is_unsigned() should be passed to this argument. This argument can + * be ignored if the first one is false, because it only applies to integer. + * + * Opt with 2 arguments: T opt(true/false is_floating_point, index/key) + * + The first argument is for determining whether the type T is a floating + * point type. That is, the result of std::is_floating_point() should be + * passed to this argument. When false, the type _should_ be a std::string. + */ + +template +T opt(std::false_type is_floating_point, int index); + +template<> +std::string opt(std::false_type /*is_floating_point*/, int index) { + return __testlib_indexToArgv(index); +} + +template +T opt(std::true_type /*is_floating_point*/, int index) { + return T(optValueToLongDouble(__testlib_indexToArgv(index))); +} + +template +T opt(std::false_type /*is_integral*/, U /*is_unsigned*/, int index) { + return opt(std::is_floating_point(), index); +} + +template +T opt(std::true_type /*is_integral*/, std::false_type /*is_unsigned*/, int index) { + return optValueToIntegral(__testlib_indexToArgv(index), false); +} + +template +T opt(std::true_type /*is_integral*/, std::true_type /*is_unsigned*/, int index) { + return optValueToIntegral(__testlib_indexToArgv(index), true); +} + +template<> +bool opt(std::true_type /*is_integral*/, std::true_type /*is_unsigned*/, int index) { + std::string value = __testlib_indexToArgv(index); + if (value == "true" || value == "1") + return true; + if (value == "false" || value == "0") + return false; + __testlib_fail("Opts: opt by index '" + toString(index) + "': expected bool true/false or 0/1 but '" + + compress(value) + "' found"); +} + +/** + * Return the parsed argv by a given index. + */ +template +T opt(int index) { + return opt(std::is_integral(), std::is_unsigned(), index); +} + +/** + * Return the raw string value of an argv by a given index. + */ +std::string opt(int index) { + return opt(index); +} + +/** + * Return the parsed argv by a given index. If the index is bigger than + * the number of argv, return the given default_value. + */ +template +T opt(int index, const T &default_value) { + if (index >= int(__testlib_argv.size())) { + return default_value; + } + return opt(index); +} + +/** + * Return the raw string value of an argv by a given index. If the index is + * bigger than the number of argv, return the given default_value. + */ +std::string opt(int index, const std::string &default_value) { + return opt(index, default_value); +} + +template +T opt(std::false_type is_floating_point, const std::string &key); + +template<> +std::string opt(std::false_type /*is_floating_point*/, const std::string &key) { + return __testlib_keyToOpts(key); +} + +template +T opt(std::true_type /*is_integral*/, const std::string &key) { + return T(optValueToLongDouble(__testlib_keyToOpts(key))); +} + +template +T opt(std::false_type /*is_integral*/, U, const std::string &key) { + return opt(std::is_floating_point(), key); +} + +template +T opt(std::true_type /*is_integral*/, std::false_type /*is_unsigned*/, const std::string &key) { + return optValueToIntegral(__testlib_keyToOpts(key), false); +} + +template +T opt(std::true_type /*is_integral*/, std::true_type /*is_unsigned*/, const std::string &key) { + return optValueToIntegral(__testlib_keyToOpts(key), true); +} + +template<> +bool opt(std::true_type /*is_integral*/, std::true_type /*is_unsigned*/, const std::string &key) { + if (!has_opt(key)) + return false; + std::string value = __testlib_keyToOpts(key); + if (value == "true" || value == "1") + return true; + if (value == "false" || value == "0") + return false; + __testlib_fail("Opts: key '" + compress(key) + "': expected bool true/false or 0/1 but '" + + compress(value) + "' found"); +} + +/** + * Return the parsed opt by a given key. + */ +template +T opt(const std::string &key) { + return opt(std::is_integral(), std::is_unsigned(), key); +} + +/** + * Return the raw string value of an opt by a given key + */ +std::string opt(const std::string &key) { + return opt(key); +} + +/* Scorer started. */ + +enum TestResultVerdict { + SKIPPED, + OK, + WRONG_ANSWER, + RUNTIME_ERROR, + TIME_LIMIT_EXCEEDED, + IDLENESS_LIMIT_EXCEEDED, + MEMORY_LIMIT_EXCEEDED, + COMPILATION_ERROR, + CRASHED, + FAILED +}; + +std::string serializeVerdict(TestResultVerdict verdict) { + switch (verdict) { + case SKIPPED: return "SKIPPED"; + case OK: return "OK"; + case WRONG_ANSWER: return "WRONG_ANSWER"; + case RUNTIME_ERROR: return "RUNTIME_ERROR"; + case TIME_LIMIT_EXCEEDED: return "TIME_LIMIT_EXCEEDED"; + case IDLENESS_LIMIT_EXCEEDED: return "IDLENESS_LIMIT_EXCEEDED"; + case MEMORY_LIMIT_EXCEEDED: return "MEMORY_LIMIT_EXCEEDED"; + case COMPILATION_ERROR: return "COMPILATION_ERROR"; + case CRASHED: return "CRASHED"; + case FAILED: return "FAILED"; + } + throw "Unexpected verdict"; +} + +TestResultVerdict deserializeTestResultVerdict(std::string s) { + if (s == "SKIPPED") + return SKIPPED; + else if (s == "OK") + return OK; + else if (s == "WRONG_ANSWER") + return WRONG_ANSWER; + else if (s == "RUNTIME_ERROR") + return RUNTIME_ERROR; + else if (s == "TIME_LIMIT_EXCEEDED") + return TIME_LIMIT_EXCEEDED; + else if (s == "IDLENESS_LIMIT_EXCEEDED") + return IDLENESS_LIMIT_EXCEEDED; + else if (s == "MEMORY_LIMIT_EXCEEDED") + return MEMORY_LIMIT_EXCEEDED; + else if (s == "COMPILATION_ERROR") + return COMPILATION_ERROR; + else if (s == "CRASHED") + return CRASHED; + else if (s == "FAILED") + return FAILED; + ensuref(false, "Unexpected serialized TestResultVerdict"); + // No return actually. + return FAILED; +} + +struct TestResult { + int testIndex; + std::string testset; + std::string group; + TestResultVerdict verdict; + double points; + long long timeConsumed; + long long memoryConsumed; + std::string input; + std::string output; + std::string answer; + int exitCode; + std::string checkerComment; +}; + +std::string serializePoints(double points) { + if (std::isnan(points)) + return ""; + else { + char c[64]; + snprintf(c, 64, "%.03lf", points); + return c; + } +} + +double deserializePoints(std::string s) { + if (s.empty()) + return std::numeric_limits::quiet_NaN(); + else { + double result; + ensuref(sscanf(s.c_str(), "%lf", &result) == 1, "Invalid serialized points"); + return result; + } +} + +std::string escapeTestResultString(std::string s) { + std::string result; + for (size_t i = 0; i < s.length(); i++) { + if (s[i] == '\r') + continue; + if (s[i] == '\n') { + result += "\\n"; + continue; + } + if (s[i] == '\\' || s[i] == ';') + result += '\\'; + result += s[i]; + } + return result; +} + +std::string unescapeTestResultString(std::string s) { + std::string result; + for (size_t i = 0; i < s.length(); i++) { + if (s[i] == '\\' && i + 1 < s.length()) { + if (s[i + 1] == 'n') { + result += '\n'; + i++; + continue; + } else if (s[i + 1] == ';' || s[i + 1] == '\\') { + result += s[i + 1]; + i++; + continue; + } + } + result += s[i]; + } + return result; +} + +std::string serializeTestResult(TestResult tr) { + std::string result; + result += std::to_string(tr.testIndex); + result += ";"; + result += escapeTestResultString(tr.testset); + result += ";"; + result += escapeTestResultString(tr.group); + result += ";"; + result += serializeVerdict(tr.verdict); + result += ";"; + result += serializePoints(tr.points); + result += ";"; + result += std::to_string(tr.timeConsumed); + result += ";"; + result += std::to_string(tr.memoryConsumed); + result += ";"; + result += escapeTestResultString(tr.input); + result += ";"; + result += escapeTestResultString(tr.output); + result += ";"; + result += escapeTestResultString(tr.answer); + result += ";"; + result += std::to_string(tr.exitCode); + result += ";"; + result += escapeTestResultString(tr.checkerComment); + return result; +} + +TestResult deserializeTestResult(std::string s) { + std::vector items; + std::string t; + for (size_t i = 0; i < s.length(); i++) { + if (s[i] == '\\') { + t += s[i]; + if (i + 1 < s.length()) + t += s[i + 1]; + i++; + continue; + } else { + if (s[i] == ';') { + items.push_back(t); + t = ""; + } else + t += s[i]; + } + } + items.push_back(t); + + ensuref(items.size() == 12, "Invalid TestResult serialization: expected exactly 12 items"); + + TestResult tr; + size_t pos = 0; + tr.testIndex = stoi(items[pos++]); + tr.testset = unescapeTestResultString(items[pos++]); + tr.group = unescapeTestResultString(items[pos++]); + tr.verdict = deserializeTestResultVerdict(items[pos++]); + tr.points = deserializePoints(items[pos++]); + tr.timeConsumed = stoll(items[pos++]); + tr.memoryConsumed = stoll(items[pos++]); + tr.input = unescapeTestResultString(items[pos++]); + tr.output = unescapeTestResultString(items[pos++]); + tr.answer = unescapeTestResultString(items[pos++]); + tr.exitCode = stoi(items[pos++]); + tr.checkerComment = unescapeTestResultString(items[pos++]); + + return tr; +} + +std::vector readTestResults(std::string fileName) { + std::ifstream stream; + stream.open(fileName.c_str(), std::ios::in); + ensuref(stream.is_open(), "Can't read test results file '%s'", fileName.c_str()); + std::vector result; + std::string line; + while (getline(stream, line)) + if (!line.empty()) + result.push_back(deserializeTestResult(line)); + stream.close(); + return result; +} + +std::function)> __testlib_scorer; + +struct TestlibScorerGuard { + ~TestlibScorerGuard() { + if (testlibMode == _scorer) { + std::vector testResults; + while (!inf.eof()) { + std::string line = inf.readLine(); + if (!line.empty()) + testResults.push_back(deserializeTestResult(line)); + } + inf.readEof(); + printf("%.3f\n", __testlib_scorer(testResults)); + } + } +} __testlib_scorer_guard; + +void registerScorer(int argc, char *argv[], std::function)> scorer) { + /* Suppress unused. */ + (void)(argc), (void)(argv); + + __testlib_ensuresPreconditions(); + + testlibMode = _scorer; + __testlib_set_binary(stdin); + + inf.init(stdin, _input); + inf.strict = false; + + __testlib_scorer = scorer; +} + +/* Scorer ended. */ + +/** + * Return the parsed opt by a given key. If no opts with the given key are + * found, return the given default_value. + * + * By calling this function, automatic ensurement for no unused opts will be + * done when the program is finalized. Call suppressEnsureNoUnusedOpts() to + * turn it off. + */ +template +T opt(const std::string &key, const T &default_value) { + if (!has_opt(key)) { + return default_value; + } + return opt(key); +} + +/** + * Return the raw string value of an opt by a given key. If no opts with the + * given key are found, return the given default_value. + * + * By calling this function, automatic ensurement for no unused opts will be + * done when the program is finalized. Call suppressEnsureNoUnusedOpts() to + * turn it off. + */ +std::string opt(const std::string &key, const std::string &default_value) { + return opt(key, default_value); +} + +/** + * Check if all opts are used. If not, __testlib_fail is called. + * Should be used after calling all opt() function calls. + * + * This function is useful when opt() with default_value for checking typos + * in the opt's key. + */ +void ensureNoUnusedOpts() { + for (const auto &opt: __testlib_opts) { + if (!opt.second.used) { + __testlib_fail(format("Opts: unused key '%s'", compress(opt.first).c_str())); + } + } +} + +void suppressEnsureNoUnusedOpts() { + __testlib_ensureNoUnusedOptsSuppressed = true; +} + +void TestlibFinalizeGuard::autoEnsureNoUnusedOpts() { + if (__testlib_ensureNoUnusedOptsFlag && !__testlib_ensureNoUnusedOptsSuppressed) { + ensureNoUnusedOpts(); + } +} + +TestlibFinalizeGuard testlibFinalizeGuard; + +#endif +#endif diff --git a/exam-queue-17/files/tests/validator-tests/01 b/exam-queue-17/files/tests/validator-tests/01 new file mode 100644 index 0000000..89161bd --- /dev/null +++ b/exam-queue-17/files/tests/validator-tests/01 @@ -0,0 +1,8 @@ +7 6 +1 2 3 4 5 6 7 +1 8 3 +2 9 +3 3 +1 3 9 +2 10 +3 1 diff --git a/exam-queue-17/files/tests/validator-tests/02 b/exam-queue-17/files/tests/validator-tests/02 new file mode 100644 index 0000000..e2ccfe4 --- /dev/null +++ b/exam-queue-17/files/tests/validator-tests/02 @@ -0,0 +1,3 @@ +1 1 +1 +1 1 1 diff --git a/exam-queue-17/files/tests/validator-tests/03 b/exam-queue-17/files/tests/validator-tests/03 new file mode 100644 index 0000000..554f86a --- /dev/null +++ b/exam-queue-17/files/tests/validator-tests/03 @@ -0,0 +1,3 @@ +1 1 +1 +2 1 diff --git a/exam-queue-17/files/tests/validator-tests/04 b/exam-queue-17/files/tests/validator-tests/04 new file mode 100644 index 0000000..ddf9364 --- /dev/null +++ b/exam-queue-17/files/tests/validator-tests/04 @@ -0,0 +1,4 @@ +2 2 +1 2 +1 3 2 +3 4 diff --git a/exam-queue-17/files/towin.exe b/exam-queue-17/files/towin.exe new file mode 100644 index 0000000..4abc661 Binary files /dev/null and b/exam-queue-17/files/towin.exe differ diff --git a/exam-queue-17/files/tutorial.tex b/exam-queue-17/files/tutorial.tex new file mode 100644 index 0000000..4d7fc56 --- /dev/null +++ b/exam-queue-17/files/tutorial.tex @@ -0,0 +1,11 @@ +\begin{tutorial}{${problem.name}} + +<#if providedTutorialCommands?? && providedTutorialCommands?size != 0><#-- + --><#list providedTutorialCommands as command><#-- + -->${command?string} + + + +${problem.tutorial} + +\end{tutorial} diff --git a/exam-queue-17/files/val.cpp b/exam-queue-17/files/val.cpp new file mode 100644 index 0000000..59a6cc2 --- /dev/null +++ b/exam-queue-17/files/val.cpp @@ -0,0 +1,52 @@ +#include +#include "testlib.h" + +using namespace std; + +int minN = 1, maxN = 1e5; +int minM = 1, maxM = 1e5; +int minA = 1, maxA = 1e9; + +int main(int argc, char* argv[]) { + registerValidation(argc, argv); + int n = inf.readInt(minN, maxN, "n"); + inf.readSpace(); + int m = inf.readInt(minM, maxM, "m"); + inf.readEoln(); + set st; + for (int i = 0; i < n; i++) { + int a = inf.readInt(minA, maxA, "a"); + if (i == n - 1) + inf.readEoln(); + else + inf.readSpace(); + st.insert(a); + } + inf.ensuref((int)st.size() == n, "a is unique"); + for (int i = 0; i < m; i++) { + int t = inf.readInt(1, 3, "t"); + inf.readSpace(); + if (t == 1) { + int x = inf.readInt(minA, maxA, "x"); + inf.readSpace(); + int y = inf.readInt(minA, maxA, "y"); + inf.ensuref(st.count(x) == 0, "x is already in list"); + inf.ensuref(st.count(y) == 1, "y is not in list"); + st.insert(x); + inf.readEoln(); + } + if (t == 2) { + int x = inf.readInt(minA, maxA, "x"); + inf.ensuref(st.count(x) == 0, "xx is already in list"); + st.insert(x); + inf.readEoln(); + } + if (t == 3) { + int x = inf.readInt(minA, maxA, "x"); + inf.ensuref(st.count(x) == 1, "x is not in list"); + st.erase(x); + inf.readEoln(); + } + } + inf.readEof(); +} diff --git a/exam-queue-17/files/val.exe b/exam-queue-17/files/val.exe new file mode 100644 index 0000000..9e1458b Binary files /dev/null and b/exam-queue-17/files/val.exe differ diff --git a/exam-queue-17/problem.xml b/exam-queue-17/problem.xml new file mode 100644 index 0000000..424bf06 --- /dev/null +++ b/exam-queue-17/problem.xml @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + 1000 + 268435456 + 51 + tests/%02d + tests/%02d.a + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + files/tests/checker-tests/%02d + files/tests/checker-tests/%02d.o + files/tests/checker-tests/%02d.a + + + + + + + + + 4 + files/tests/validator-tests/%02d + + + + + + + + + + + + + + + + + + + + + + + + + 0 + stresses/%03d + + + + + + diff --git a/exam-queue-17/scripts/gen-answer.bat b/exam-queue-17/scripts/gen-answer.bat new file mode 100644 index 0000000..dd310be --- /dev/null +++ b/exam-queue-17/scripts/gen-answer.bat @@ -0,0 +1,19 @@ +set argumentCount=0 +for %%x in (%*) do Set /A argumentCount+=1 +if not "%argumentCount%"=="4" pause 0 +if "%1"=="" pause 0 +if "%2"=="" pause 0 +if not exist %1 pause 0 +files\val.exe --testset "%~3" --group "%~4" < %1 +if errorlevel 1 pause 0 +solutions\valavshonok_OK.exe < %1 > output.txt +if errorlevel 1 pause 0 +if not exist output.txt pause 0 +move output.txt %2 +check.exe %1 %2 %2 +:start +set error=1 +if %errorlevel% equ 0 set error=0 +if %errorlevel% equ 7 set error=0 +if %error% equ 1 pause 0 +:end diff --git a/exam-queue-17/scripts/gen-answer.sh b/exam-queue-17/scripts/gen-answer.sh new file mode 100644 index 0000000..2b2c928 --- /dev/null +++ b/exam-queue-17/scripts/gen-answer.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +if [ "$#" -ne "4" ]; then + read +fi +if [ "$1" = "" ]; then + read +fi +if [ "$2" = "" ]; then + read +fi +if [ ! -f "$1" ]; then + read +fi +echo "Running validator" +wine files/towin.exe "$1" | wine files/val.exe --testset "$3" --group "$4" +if [ "$?" -ne "0" ]; then + read +fi +echo "Running solution valavshonok_OK.cpp" +wine solutions/valavshonok_OK.exe < "$1" > output.txt +if [ "$?" -ne "0" ]; then + echo "Solution returned non-zero exit code" + read +fi +if [ ! -f "output.txt" ]; then + echo "Solution didn't produced output" + read +fi +mv output.txt "$2" +echo "Running checker" +wine check.exe "$1" "$2" "$2" +if [ "$?" -ne "0" ] && [ "$?" -ne "7" ]; then + echo "Checker exit code is not equal to 0 and 7" + read +fi diff --git a/exam-queue-17/scripts/gen-input-via-file.bat b/exam-queue-17/scripts/gen-input-via-file.bat new file mode 100644 index 0000000..aca0c06 --- /dev/null +++ b/exam-queue-17/scripts/gen-input-via-file.bat @@ -0,0 +1,22 @@ +rem parameter 1 is generator execution command line from the root of the package +rem parameter 2 is test input file path from the root of the package +rem parameter 3 is the test index + +if "%~1"=="" pause 0 +if "%~2"=="" pause 0 +if "%~3"=="" pause 0 + +del /F /Q "%~2" + +if exist tmp-for-generator-execution rd /S /Q tmp-for-generator-execution +md tmp-for-generator-execution +cd tmp-for-generator-execution + +%~1 + +if errorlevel 1 pause 0 +if exist "%~3" copy "%~3" "..\%~2" +cd .. +rd /S /Q tmp-for-generator-execution + +if not exist "%~2" pause 0 diff --git a/exam-queue-17/scripts/gen-input-via-file.sh b/exam-queue-17/scripts/gen-input-via-file.sh new file mode 100644 index 0000000..e45ae70 --- /dev/null +++ b/exam-queue-17/scripts/gen-input-via-file.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +if [ "$1" = "" ] +then + echo "First parameter is empty" + read +fi + +if [ "$2" = "" ] +then + echo "Second parameter is empty" + read +fi + +if [ "$3" = "" ] +then + echo "Third parameter is empty" + read +fi + +rm -f "$2" + +rm -rf tmp-for-input-generation +mkdir tmp-for-input-generation +cd tmp-for-input-generation + +eval $1 + +if [ "$?" -ne "0" ] +then + echo "Executed "../"$1"", but it returns non-zero exit code" + read +fi + +cp "$3" "../$2" + +if [ "$?" -ne "0" ] +then + echo "Can't copy $3 to ../$2" + read +fi + +if [ ! -f "../$2" ] +then + echo "Can't find ../$2" + read +fi + +cd .. +rm -rf tmp-for-input-generation diff --git a/exam-queue-17/scripts/gen-input-via-files.bat b/exam-queue-17/scripts/gen-input-via-files.bat new file mode 100644 index 0000000..17beb33 --- /dev/null +++ b/exam-queue-17/scripts/gen-input-via-files.bat @@ -0,0 +1,70 @@ +@echo off + +rem parameter 1 is generator execution command line from the root of the package +rem parameter 2 is test input file path from the root of the package or several paths separated with ":" +rem parameter 3 is the test index or several indices separated with ":" + +if "%~1"=="" pause 0 +if "%~2"=="" pause 0 +if "%~3"=="" pause 0 + +if exist tmp-for-generator-execution rd /S /Q tmp-for-generator-execution +md tmp-for-generator-execution +cd tmp-for-generator-execution +%~1 +if errorlevel 1 pause 0 + +setlocal ENABLEDELAYEDEXPANSION + +set paths=%~2 +set indices=%~3 + +:tokenLoop + +if "!paths!" EQU "" goto splitEnd +if "!indices!" EQU "" goto splitEnd + +for /f "delims=:" %%a in ("!paths!") do set pathItem=%%a +for /f "delims=:" %%a in ("!indices!") do set indexItem=%%a + +if exist "..\!pathItem!" del /F /Q "..\!pathItem!" +set copied=0 +if exist "!indexItem!" ( + set copied=1 + copy "!indexItem!" "..\!pathItem!" > nul +) +if exist "0!indexItem!" ( + set copied=1 + copy "0!indexItem!" "..\!pathItem!" > nul +) +if exist "00!indexItem!" ( + set copied=1 + copy "00!indexItem!" "..\!pathItem!" > nul +) +echo %copied% +if "%copied%"=="0" ( + echo Unable to find test !indexItem! + pause 0 +) + +echo Test #!indexItem! has been generated and copied to !pathItem! + +:pathsStripLoop +set pathsFirstChar=!paths:~0,1! +set paths=!paths:~1! +if "!paths!" EQU "" goto splitEnd +if "!pathsFirstChar!" NEQ ":" goto pathsStripLoop + +:indicesStripLoop +set indicesFirstChar=!indices:~0,1! +set indices=!indices:~1! +if "!indices!" EQU "" goto splitEnd +if "!indicesFirstChar!" NEQ ":" goto indicesStripLoop + +goto tokenLoop +:splitEnd + +endlocal + +cd .. +rd /S /Q tmp-for-generator-execution diff --git a/exam-queue-17/scripts/gen-input-via-files.sh b/exam-queue-17/scripts/gen-input-via-files.sh new file mode 100644 index 0000000..eb93763 --- /dev/null +++ b/exam-queue-17/scripts/gen-input-via-files.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash + +# parameter 1 is generator execution command line from the root of the package +# parameter 2 is test input file path from the root of the package or several paths separated with ":" +# parameter 3 is the test index or several indices separated with ":" + +if [ "$1" = "" ] +then + echo "First parameter is empty" + read +fi + +if [ "$2" = "" ] +then + echo "Second parameter is empty" + read +fi + +if [ "$3" = "" ] +then + echo "Third parameter is empty" + read +fi + +rm -rf tmp-for-input-generation +mkdir tmp-for-input-generation +cd tmp-for-input-generation + +eval "$1" + +if [ "$?" -ne "0" ] +then + echo "Executed "../"$1"", but it returns non-zero exit code" + read +fi + +paths=($(echo "$2" | tr ":" "\n")) +indices=($(echo "$3" | tr ":" "\n")) + +for (( i = 0 ; i < ${#paths[@]} ; i++ )) +do +rm -f "../${paths[$i]}" + +cp "${indices[$i]}" "../${paths[$i]}" 2> /dev/null || +cp "0${indices[$i]}" "../${paths[$i]}" 2> /dev/null || +cp "00${indices[$i]}" "../${paths[$i]}" 2> /dev/null + +if [ "$?" -ne "0" ] +then + echo "Can't copy ${indices[$i]} to ../${paths[$i]}" + read +fi + +if [ ! -f "../${paths[$i]}" ] +then + echo "Can't find ../${paths[$i]}" + read +fi + +echo "Test #${indices[$i]} has been generated and copied to ${paths[$i]}" + +done + +cd .. +rm -rf tmp-for-input-generation diff --git a/exam-queue-17/scripts/gen-input-via-stdout.bat b/exam-queue-17/scripts/gen-input-via-stdout.bat new file mode 100644 index 0000000..1291190 --- /dev/null +++ b/exam-queue-17/scripts/gen-input-via-stdout.bat @@ -0,0 +1,10 @@ +rem %1 is generator execution command line from the root of the package +rem %2 is test input file path from the root of the package +rem %3 is the test index + +if "%~1"=="" pause 0 +if "%~2"=="" pause 0 +del /F /Q "%~2" +%~1 > "%~2" +if errorlevel 1 pause 0 +if not exist "%~2" pause 0 diff --git a/exam-queue-17/scripts/gen-input-via-stdout.sh b/exam-queue-17/scripts/gen-input-via-stdout.sh new file mode 100644 index 0000000..e2f7965 --- /dev/null +++ b/exam-queue-17/scripts/gen-input-via-stdout.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +if [ "$1" = "" ] +then + echo "First parameter is empty" + read +fi + +if [ "$2" = "" ] +then + echo "Second parameter is empty" + read +fi + +rm -f "$2" + +eval $1 > $2 + +if [ "$?" -ne "0" ] +then + echo "Executed $1, but it returns non-zero exit code" + read +fi + +if [ ! -f "$2" ] +then + echo "Executed ""$1" > "$2"", but it didn't create file" + read +fi diff --git a/exam-queue-17/scripts/run-checker-tests.bat b/exam-queue-17/scripts/run-checker-tests.bat new file mode 100644 index 0000000..ec8c7c9 --- /dev/null +++ b/exam-queue-17/scripts/run-checker-tests.bat @@ -0,0 +1,4 @@ +echo Running 0 checker test(s) +echo Running 0 checker test(s) 1> checker-tests.log +del /F /Q checker-tests.log +echo Checker test(s) finished diff --git a/exam-queue-17/scripts/run-checker-tests.sh b/exam-queue-17/scripts/run-checker-tests.sh new file mode 100644 index 0000000..4e144e2 --- /dev/null +++ b/exam-queue-17/scripts/run-checker-tests.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +echo "Running 0 checker test(s)" +echo "Running 0 checker test(s)" 1> checker-tests.log +rm -f checker-tests.log +echo "Checker test(s) finished" diff --git a/exam-queue-17/scripts/run-validator-tests.bat b/exam-queue-17/scripts/run-validator-tests.bat new file mode 100644 index 0000000..169efc4 --- /dev/null +++ b/exam-queue-17/scripts/run-validator-tests.bat @@ -0,0 +1,36 @@ +echo Running 4 validator test(s) +echo Running 4 validator test(s) 1> validator-tests.log +echo Running test #1 1>> validator-tests.log +echo Validator comment: 1>> validator-tests.log +files\val.exe < files\tests\validator-tests\01 2>> validator-tests.log +if errorlevel 1 ( + echo Validator returned non-zero exit code for a valid test 1>> validator-tests.log + echo Validator returned non-zero exit code for a valid test. See validator-tests.log for validator comment + pause 0 +) +echo Running test #2 1>> validator-tests.log +echo Validator comment: 1>> validator-tests.log +files\val.exe < files\tests\validator-tests\02 2>> validator-tests.log +if not errorlevel 1 ( + echo Validator returned zero exit code for a invalid test 1>> validator-tests.log + echo Validator returned zero exit code for a invalid test. See validator-tests.log for validator comment + pause 0 +) +echo Running test #3 1>> validator-tests.log +echo Validator comment: 1>> validator-tests.log +files\val.exe < files\tests\validator-tests\03 2>> validator-tests.log +if not errorlevel 1 ( + echo Validator returned zero exit code for a invalid test 1>> validator-tests.log + echo Validator returned zero exit code for a invalid test. See validator-tests.log for validator comment + pause 0 +) +echo Running test #4 1>> validator-tests.log +echo Validator comment: 1>> validator-tests.log +files\val.exe < files\tests\validator-tests\04 2>> validator-tests.log +if not errorlevel 1 ( + echo Validator returned zero exit code for a invalid test 1>> validator-tests.log + echo Validator returned zero exit code for a invalid test. See validator-tests.log for validator comment + pause 0 +) +del /F /Q validator-tests.log +echo Validator test(s) finished diff --git a/exam-queue-17/scripts/run-validator-tests.sh b/exam-queue-17/scripts/run-validator-tests.sh new file mode 100644 index 0000000..3175d5a --- /dev/null +++ b/exam-queue-17/scripts/run-validator-tests.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +echo "Running 4 validator test(s)" +echo "Running 4 validator test(s)" 1> validator-tests.log +echo "Running test #1" 1>> validator-tests.log +echo "Validator comment:" 1>> validator-tests.log +wine files/val.exe < files/tests/validator-tests/01 2>> validator-tests.log +if [ "$?" -ne "0" ]; then + echo "Validator returned non-zero exit code for a valid test" 1>> validator-tests.log + echo "Validator returned non-zero exit code for a valid test. See validator-tests.log for validator comment" + read +fi +echo "Running test #2" 1>> validator-tests.log +echo "Validator comment:" 1>> validator-tests.log +wine files/val.exe < files/tests/validator-tests/02 2>> validator-tests.log +if [ "$?" -eq "0" ]; then + echo "Validator returned zero exit code for a invalid test" 1>> validator-tests.log + echo "Validator returned zero exit code for a invalid test. See validator-tests.log for validator comment" + read +fi +echo "Running test #3" 1>> validator-tests.log +echo "Validator comment:" 1>> validator-tests.log +wine files/val.exe < files/tests/validator-tests/03 2>> validator-tests.log +if [ "$?" -eq "0" ]; then + echo "Validator returned zero exit code for a invalid test" 1>> validator-tests.log + echo "Validator returned zero exit code for a invalid test. See validator-tests.log for validator comment" + read +fi +echo "Running test #4" 1>> validator-tests.log +echo "Validator comment:" 1>> validator-tests.log +wine files/val.exe < files/tests/validator-tests/04 2>> validator-tests.log +if [ "$?" -eq "0" ]; then + echo "Validator returned zero exit code for a invalid test" 1>> validator-tests.log + echo "Validator returned zero exit code for a invalid test. See validator-tests.log for validator comment" + read +fi +rm -f validator-tests.log +echo "Validator test(s) finished" diff --git a/exam-queue-17/solutions/nyatl_ok.cpp b/exam-queue-17/solutions/nyatl_ok.cpp new file mode 100644 index 0000000..7f39af6 --- /dev/null +++ b/exam-queue-17/solutions/nyatl_ok.cpp @@ -0,0 +1,62 @@ +#include +#include +#include +#include + +using namespace std; + +#define MAX 101000 + +int n, k; +int a[MAX]; +map nxt; +map prv; + +int main() { + scanf("%d %d", &n, &k); + for (int i = 1; i <= n; i++) { + scanf("%d", &a[i]); + } + for (int i = 1; i < n; i++) { + nxt[a[i]] = a[i + 1]; + } + for (int i = 2; i <= n; i++) { + prv[a[i]] = a[i - 1]; + } + int last = a[n]; + for (int i = 1; i <= k; i++) { + int type; + scanf("%d", &type); + if (type == 1) { + int x, y; + scanf("%d %d", &x, &y); + tie(nxt[x], prv[x], nxt[prv[y]], prv[y]) = {y, prv[y], x, x}; + } else if (type == 2) { + int x; + scanf("%d", &x); + nxt[last] = x; + prv[x] = last; + last = x; + } else if (type == 3) { + int x; + scanf("%d", &x); + tie(nxt[prv[x]], prv[nxt[x]]) = {nxt[x], prv[x]}; + if (last == x) last = prv[x]; + nxt[x] = prv[x] = 0; + } + } + + vector ans; + int x = last; + while (x != 0) { + ans.push_back(x); + x = prv[x]; + } + printf("%d\n", (int)ans.size()); + for (int i = (int)ans.size() - 1; i >= 0; i--) { + printf("%d ", ans[i]); + } + printf("\n"); + + return 0; +} \ No newline at end of file diff --git a/exam-queue-17/solutions/nyatl_ok.cpp.desc b/exam-queue-17/solutions/nyatl_ok.cpp.desc new file mode 100644 index 0000000..e8a5267 --- /dev/null +++ b/exam-queue-17/solutions/nyatl_ok.cpp.desc @@ -0,0 +1,4 @@ +File name: nyatl_ok.cpp +Tag: ACCEPTED +Author: Nyatl +Change time: Mon Apr 07 21:16:54 MSK 2025 diff --git a/exam-queue-17/solutions/nyatl_ok.exe b/exam-queue-17/solutions/nyatl_ok.exe new file mode 100644 index 0000000..4d094da Binary files /dev/null and b/exam-queue-17/solutions/nyatl_ok.exe differ diff --git a/exam-queue-17/solutions/valavshonok_OK.cpp b/exam-queue-17/solutions/valavshonok_OK.cpp new file mode 100644 index 0000000..a5b9a36 --- /dev/null +++ b/exam-queue-17/solutions/valavshonok_OK.cpp @@ -0,0 +1,54 @@ +#include + +using namespace std; + +void F() { + int n, m; + cin >> n >> m; + list a; + map::iterator> mp; + for (int i = 0; i < n; i++) { + int b; + cin >> b; + a.push_back(b); + mp[b] = prev(a.end()); + } + while (m--) { + int t; + cin >> t; + if (t == 1) { + int x, y; + cin >> x >> y; + a.insert(mp[y], x); + mp[x] = prev(mp[y]); + } + if (t == 2) { + int x; + cin >> x; + a.push_back(x); + mp[x] = prev(a.end()); + } + if (t == 3) { + int x; + cin >> x; + a.erase(mp[x]); + } + } + cout << a.size() << '\n'; + for (auto i : a) + cout << i << ' '; + cout << '\n'; +} + +int main() { +#ifdef KRAKOZAYBRA + FILE* stream; + freopen_s(&stream, "input.txt", "r", stdin); + //freopen_s(&stream, "output.txt", "w", stdout); +#endif + cin.tie(0)->sync_with_stdio(0); + int _t = 1; + //cin >> _t; + while (_t--) + F(); +} \ No newline at end of file diff --git a/exam-queue-17/solutions/valavshonok_OK.cpp.desc b/exam-queue-17/solutions/valavshonok_OK.cpp.desc new file mode 100644 index 0000000..3f140e6 --- /dev/null +++ b/exam-queue-17/solutions/valavshonok_OK.cpp.desc @@ -0,0 +1,4 @@ +File name: valavshonok_OK.cpp +Tag: MAIN +Author: valavshonok +Change time: Tue Apr 01 00:12:51 MSK 2025 diff --git a/exam-queue-17/solutions/valavshonok_OK.exe b/exam-queue-17/solutions/valavshonok_OK.exe new file mode 100644 index 0000000..b33721e Binary files /dev/null and b/exam-queue-17/solutions/valavshonok_OK.exe differ diff --git a/exam-queue-17/statement-sections/russian/example.01 b/exam-queue-17/statement-sections/russian/example.01 new file mode 100644 index 0000000..89161bd --- /dev/null +++ b/exam-queue-17/statement-sections/russian/example.01 @@ -0,0 +1,8 @@ +7 6 +1 2 3 4 5 6 7 +1 8 3 +2 9 +3 3 +1 3 9 +2 10 +3 1 diff --git a/exam-queue-17/statement-sections/russian/example.01.a b/exam-queue-17/statement-sections/russian/example.01.a new file mode 100644 index 0000000..75d1da5 --- /dev/null +++ b/exam-queue-17/statement-sections/russian/example.01.a @@ -0,0 +1,2 @@ +9 +2 8 4 5 6 7 3 9 10 diff --git a/exam-queue-17/statement-sections/russian/input.tex b/exam-queue-17/statement-sections/russian/input.tex new file mode 100644 index 0000000..eaae7e3 --- /dev/null +++ b/exam-queue-17/statement-sections/russian/input.tex @@ -0,0 +1,13 @@ +В первой строке заданы два целых числа $n$ и $m$ $(1 \le n, m \le 10^5)$~--- текущее число студентов в очереди и количество изменений. + +В следующей строке задается $n$ целых \textbf{различных} чисел $a_1, a_2, \cdots , a_n$ $(1 \le a_i \le 10^9)$, где $a_i$~--- номер студента, который стоит на $i$-й позиции в очереди. + +В следующих $m$ строках идет описание запросов изменения очереди. + +В каждой строке в зависимости от типа запроса задается два или три числа. Первое число $t_j$ $(1 \le t_j \le 3)$~--- тип события, которое произошло в $j$-ю минуту. + +Если $t_j = \textbf{1}$, то в строке задается еще 2 числа $x$ $(1 \le x_j \le 10^9)$ и $y$ $(1 \le y_j \le 10^9)$~--- номер студента, который пришел, и номер студента, перед которым он встанет в очереди. Гарантируется, что студент с номером $x$ ещё не занял очередь, а студент с номером $y$ уже стоит в ней. + +Если $t_j = \textbf{2}$, то в строке задается еще 1 число $x$ $(1 \le x_j \le 10^9)$~--- номер студента, который пришел и встал в конец очереди. Гарантируется, что студент с номером $x$ ещё не занял очередь. + +Если $t_j = \textbf{3}$, то в строке задается еще 1 число $x$ $(1 \le x_j \le 10^9)$~--- номер студента, который ушел из очереди. Гарантируется, что студент с номером $x$ стоит в очереди. \ No newline at end of file diff --git a/exam-queue-17/statement-sections/russian/legend.tex b/exam-queue-17/statement-sections/russian/legend.tex new file mode 100644 index 0000000..2e2f79a --- /dev/null +++ b/exam-queue-17/statement-sections/russian/legend.tex @@ -0,0 +1,16 @@ +В честь юбилея ректорат ЮФУ решил запустить акцию <<Сто и десять кексов>>. В каждом корпусе университета открылась лавка с кексами, в которой каждый студент может получить бесплатные кексы. + +Не прошло и пары минут после открытия, как к лавкам набежали студенты и образовалось много очередей. Но самая большая очередь образовалась в главном корпусе ЮФУ. Изначально в этой очереди стояло $n$ студентов, но потом в течение следующих $m$ минут какие-то студенты приходили и вставали в очередь, а какие-то уходили. + +За каждым студентом закреплен номер его зачетной книжки, будем называть это число номером студента. У каждого студента будет уникальный номер, по которому можно однозначно его идентифицировать. Будем считать, что каждую минуту происходило одно из следующих событий: + +\begin{enumerate} + \item Студент с номером $x$ пришел и встал перед студентом с номером $y$; + \item Студент с номером $x$ пришел и встал в конец очереди; + \item Студент с номером $x$ ушел из очереди; возможно, он потом вернется. +\end{enumerate} + +Аналитикам стало интересно, а какой будет очередь после $m$ минут? + +Помогите им и сообщите конечное состояние очереди. + diff --git a/exam-queue-17/statement-sections/russian/name.tex b/exam-queue-17/statement-sections/russian/name.tex new file mode 100644 index 0000000..8c0e6da --- /dev/null +++ b/exam-queue-17/statement-sections/russian/name.tex @@ -0,0 +1 @@ +Очередь за кексами \ No newline at end of file diff --git a/exam-queue-17/statement-sections/russian/notes.tex b/exam-queue-17/statement-sections/russian/notes.tex new file mode 100644 index 0000000..0c336a7 --- /dev/null +++ b/exam-queue-17/statement-sections/russian/notes.tex @@ -0,0 +1,31 @@ +Изначально очередь выглядит следующим образом: + +\includegraphics{o1.png} + +В первую минуту приходит студент с номером 8 и встает перед студентом с номером 3. + +\includegraphics{o2.png} + +Потом студент с номером 9 встает в конец очереди. + +\includegraphics{o3.png} + +Студент с номером 3 уходит из очереди. + +\includegraphics{o4.png} + +Потом он возвращается и становится перед студентом с номером 9. + +\includegraphics{o5.png} + +После в конец очереди становится студент с номером 10. + +\includegraphics{o6.png} + +И студент с номером 1 уходит из очереди. + +\includegraphics{o7.png} + +После $m$ событий очередь имеет следующий вид: + +\includegraphics{o8.png} \ No newline at end of file diff --git a/exam-queue-17/statement-sections/russian/o1.png b/exam-queue-17/statement-sections/russian/o1.png new file mode 100644 index 0000000..2b720a0 Binary files /dev/null and b/exam-queue-17/statement-sections/russian/o1.png differ diff --git a/exam-queue-17/statement-sections/russian/o2.png b/exam-queue-17/statement-sections/russian/o2.png new file mode 100644 index 0000000..75a51e0 Binary files /dev/null and b/exam-queue-17/statement-sections/russian/o2.png differ diff --git a/exam-queue-17/statement-sections/russian/o3.png b/exam-queue-17/statement-sections/russian/o3.png new file mode 100644 index 0000000..aed22b2 Binary files /dev/null and b/exam-queue-17/statement-sections/russian/o3.png differ diff --git a/exam-queue-17/statement-sections/russian/o4.png b/exam-queue-17/statement-sections/russian/o4.png new file mode 100644 index 0000000..7774783 Binary files /dev/null and b/exam-queue-17/statement-sections/russian/o4.png differ diff --git a/exam-queue-17/statement-sections/russian/o5.png b/exam-queue-17/statement-sections/russian/o5.png new file mode 100644 index 0000000..6fbbd47 Binary files /dev/null and b/exam-queue-17/statement-sections/russian/o5.png differ diff --git a/exam-queue-17/statement-sections/russian/o6.png b/exam-queue-17/statement-sections/russian/o6.png new file mode 100644 index 0000000..b424e7a Binary files /dev/null and b/exam-queue-17/statement-sections/russian/o6.png differ diff --git a/exam-queue-17/statement-sections/russian/o7.png b/exam-queue-17/statement-sections/russian/o7.png new file mode 100644 index 0000000..46ca714 Binary files /dev/null and b/exam-queue-17/statement-sections/russian/o7.png differ diff --git a/exam-queue-17/statement-sections/russian/o8.png b/exam-queue-17/statement-sections/russian/o8.png new file mode 100644 index 0000000..9028df3 Binary files /dev/null and b/exam-queue-17/statement-sections/russian/o8.png differ diff --git a/exam-queue-17/statement-sections/russian/output.tex b/exam-queue-17/statement-sections/russian/output.tex new file mode 100644 index 0000000..0ae6104 --- /dev/null +++ b/exam-queue-17/statement-sections/russian/output.tex @@ -0,0 +1,3 @@ +В первой строке выведите одно число $|a|$~--- длину очереди после выполнения всех запросов изменения. + +В следующей строке выведите $|a|$ чисел $a_1, a_2, \cdots , a_{|a|}$, где $a_i$~--- номер студента, который стоит на $i$-й позиции в очереди. \ No newline at end of file diff --git a/exam-queue-17/statement-sections/russian/tutorial.tex b/exam-queue-17/statement-sections/russian/tutorial.tex new file mode 100644 index 0000000..2557ea3 --- /dev/null +++ b/exam-queue-17/statement-sections/russian/tutorial.tex @@ -0,0 +1,15 @@ +Давайте просто промоделируем все действия. + +Заведем список элементов, а также сохраним по ключу $x$ указатель на элемент списка. Мы можем это сделать, так как все элементы различны. Например, в С++ можно просто завести коллекцию list, а также map::iterator> или реализовать свой список. + +Теперь мы можем легко обрабатывать все запросы, а в конце просто выведем весь список. + +Запрос 1-го типа можно обработать так: просто берем по ключу указатель на нужный элемент и вставляем перед ним другой элемент, останется только по ключу $x$ записать указатель на новый элемент. + +Запрос 2-го типа~--- просто добавить в список элемент в конец и сохранить на него указатель. + +Запрос 3-го типа~--- удаляем из списка элемент по его указателю. + +В конце просто выводим массив. + +Итоговая сложность $O(mlog(n))$ \ No newline at end of file diff --git a/exam-queue-17/statements/.html/russian/15c12c02bcb2f87450906d26075f1336c6f8bb79.png b/exam-queue-17/statements/.html/russian/15c12c02bcb2f87450906d26075f1336c6f8bb79.png new file mode 100644 index 0000000..727139b Binary files /dev/null and b/exam-queue-17/statements/.html/russian/15c12c02bcb2f87450906d26075f1336c6f8bb79.png differ diff --git a/exam-queue-17/statements/.html/russian/514e52cdf82b640ce29f9178bbd9be3379ab43dd.png b/exam-queue-17/statements/.html/russian/514e52cdf82b640ce29f9178bbd9be3379ab43dd.png new file mode 100644 index 0000000..41867aa Binary files /dev/null and b/exam-queue-17/statements/.html/russian/514e52cdf82b640ce29f9178bbd9be3379ab43dd.png differ diff --git a/exam-queue-17/statements/.html/russian/6f77902a9e98428961fb5c1bde374d946a82cdd2.png b/exam-queue-17/statements/.html/russian/6f77902a9e98428961fb5c1bde374d946a82cdd2.png new file mode 100644 index 0000000..c150028 Binary files /dev/null and b/exam-queue-17/statements/.html/russian/6f77902a9e98428961fb5c1bde374d946a82cdd2.png differ diff --git a/exam-queue-17/statements/.html/russian/7b0ffd2ae443ff754e3131a6ddc77af4cb17a043.png b/exam-queue-17/statements/.html/russian/7b0ffd2ae443ff754e3131a6ddc77af4cb17a043.png new file mode 100644 index 0000000..539ced9 Binary files /dev/null and b/exam-queue-17/statements/.html/russian/7b0ffd2ae443ff754e3131a6ddc77af4cb17a043.png differ diff --git a/exam-queue-17/statements/.html/russian/b65f6e3c0bcc7e96380e577e3a79156116f6947e.png b/exam-queue-17/statements/.html/russian/b65f6e3c0bcc7e96380e577e3a79156116f6947e.png new file mode 100644 index 0000000..230c2a8 Binary files /dev/null and b/exam-queue-17/statements/.html/russian/b65f6e3c0bcc7e96380e577e3a79156116f6947e.png differ diff --git a/exam-queue-17/statements/.html/russian/cb6dba7c484189e88d8e1bc0e15767bbb44c0c69.png b/exam-queue-17/statements/.html/russian/cb6dba7c484189e88d8e1bc0e15767bbb44c0c69.png new file mode 100644 index 0000000..570a62e Binary files /dev/null and b/exam-queue-17/statements/.html/russian/cb6dba7c484189e88d8e1bc0e15767bbb44c0c69.png differ diff --git a/exam-queue-17/statements/.html/russian/e29b6eb80ce5dcdb59a696421149e86cf24fff83.png b/exam-queue-17/statements/.html/russian/e29b6eb80ce5dcdb59a696421149e86cf24fff83.png new file mode 100644 index 0000000..9ef8e09 Binary files /dev/null and b/exam-queue-17/statements/.html/russian/e29b6eb80ce5dcdb59a696421149e86cf24fff83.png differ diff --git a/exam-queue-17/statements/.html/russian/ef8c0f8c7ba7108bef3d50979ae09eb267158ee6.png b/exam-queue-17/statements/.html/russian/ef8c0f8c7ba7108bef3d50979ae09eb267158ee6.png new file mode 100644 index 0000000..33bec1e Binary files /dev/null and b/exam-queue-17/statements/.html/russian/ef8c0f8c7ba7108bef3d50979ae09eb267158ee6.png differ diff --git a/exam-queue-17/statements/.html/russian/problem-statement.css b/exam-queue-17/statements/.html/russian/problem-statement.css new file mode 100644 index 0000000..bf975b9 --- /dev/null +++ b/exam-queue-17/statements/.html/russian/problem-statement.css @@ -0,0 +1,318 @@ +.problem-statement { + margin: 0.5em auto 2em auto; + font-family: verdana,serif; + line-height: 1.5em; + font-size: 14px; + max-width: 1024px; +} + +.problem-statement .epigraph { + margin-left: 67%; + width: 33%; +} + +.problem-statement .epigraph-text { +} + +.problem-statement .epigraph-source { + border-top: 1px solid #888; + text-align: right; +} + +.problem-statement .lstlisting { + padding: 0.5em; + background-color: #f2f2f2; +} + +.problem-statement .tex-tabular { + margin: 1em 0; + border-collapse: collapse; + border-spacing: 0; +} + +.problem-statement .tex-tabular td { + padding: 0.15em 0.7em; +} + +.problem-statement .tex-tabular .tex-tabular-border-left { + border-left: 1px solid; +} + +.problem-statement .tex-tabular .tex-tabular-border-right { + border-right: 1px solid; +} + +.problem-statement .tex-tabular .tex-tabular-border-top { + border-top: 1px solid; +} + +.problem-statement .tex-tabular .tex-tabular-border-bottom { + border-bottom: 1px solid; +} + +.problem-statement .tex-tabular .tex-tabular-text-align-left { + text-align: left; +} + +.problem-statement .tex-tabular .tex-tabular-text-align-center { + text-align: center; +} + +.problem-statement .tex-tabular .tex-tabular-text-align-right { + text-align: right; +} + +.problem-statement p { + margin: 0 0 1em 0; +} + +.problem-statement p a, .problem-statement ul a, .problem-statement ol a, .problem-statement td a { + margin: 0; +} + +.problem-statement .header { + margin-bottom: 1em; + text-align: center; +} + +.problem-statement .header .title { + font-size: 150%; + margin-bottom: 0.25em; +} + +.problem-statement .header .title { + font-size: 150%; + font-family: arial, serif; +} + +.problem-statement ul { + list-style: disc outside; + margin: 0 0 1em 0; +} + +.problem-statement ol { + list-style: decimal outside; + margin: 0 0 1em 0; +} + +.problem-statement li { + line-height: 1.5em; + margin-left: 3em; +} + +.problem-statement .property-title { + display: inline; +} + +.problem-statement .property-title:after { + content: ": "; +} + +.problem-statement .time-limit, .problem-statement .memory-limit, .problem-statement .input-file, .problem-statement .output-file { + margin: 0 auto; +} + +.problem-statement .legend { + margin-bottom: 1em; +} + +.problem-statement .tutorial { + margin-bottom: 1em; +} + +.problem-statement .section-title { + font-family: arial, serif; + font-size: 115%; + font-weight: bold; +} + +.problem-statement .input-specification, + .problem-statement .output-specification, + .problem-statement .sample-tests, + .problem-statement .author, + .problem-statement .resource, + .problem-statement .date { + /*margin-bottom: 1em;*/ +} + +.problem-statement .output-specification { + margin-bottom: 1em; +} + +.problem-statement .sample-tests .sample-test { +} + +.problem-statement .sample-tests .input, .problem-statement .sample-tests .output { + border: 1px solid #888; +} + +.problem-statement .sample-tests .output { + margin-bottom: 1em; + position: relative; + top: -1px; +} + +.problem-statement .sample-tests pre { + line-height: 1.25em; + padding: 0.25em; + margin: 0; + background-color: #efefef; +} + +.problem-statement .sample-tests .title { + font-family: arial, serif; + padding: 0.25em; + border-bottom: 1px solid #888; + text-transform: lowercase; + font-weight: bold; +} + +.problem-statement .tex-formula { + vertical-align: middle; + margin: 0; + border:medium none; + position: relative; + bottom: 2px; +} + +.problem-statement .tex-span { + font-size: 125%; + font-family: times new roman, serif; + white-space: nowrap; +} + +.problem-statement .tex-font-size-tiny { + font-size: 70%; +} + +.problem-statement .tex-font-size-script { + font-size: 75%; +} + +.problem-statement .tex-font-size-footnotes { + font-size: 85%; +} + +.problem-statement .tex-font-size-small { + font-size: 85%; +} + +.problem-statement .tex-font-size-normal { + font-size: 100%; +} + +.problem-statement .tex-font-size-large-1 { + font-size: 115%; +} + +.problem-statement .tex-font-size-large-2 { + font-size: 130%; +} + +.problem-statement .tex-font-size-large-3 { + font-size: 145%; +} + +.problem-statement .tex-font-size-huge-1 { + font-size: 175%; +} + +.problem-statement .tex-font-size-huge-2 { + font-size: 200%; +} + +.problem-statement .tex-font-style-rm { +} + +.problem-statement .tex-font-style-striked { + text-decoration: line-through; +} + +.problem-statement .tex-font-style-underline { + text-decoration: underline; +} + +.problem-statement .tex-font-style-sf { + font-family: arial, serif; +} + +.problem-statement .tex-font-style-tt { + font-size: 110%; + font-family: courier new, serif; +} + +.problem-statement .tex-font-style-md { +} + +.problem-statement .tex-font-style-bf { + font-weight: bold; +} + +.problem-statement .tex-font-style-up { +} + +.problem-statement .tex-font-style-it { + font-style: italic; +} + +.problem-statement .tex-font-style-sl { + font-style: italic; +} + +.problem-statement .tex-font-style-sc { + text-transform: uppercase; +} + +.problem-statement .tex-graphics { + max-width: 95%; + display: block; +} + +.problem-statement .tex-tabular .tex-graphics { + max-width: 100%; +} + +.problem-statement .input-output-copier { + font-size: 0.75rem; + float: right; + color: #888; + padding: 3px; + cursor: pointer; + border: 1px solid rgb(185, 185, 185); + line-height: 0.8rem; + text-transform: none; +} + +.problem-statement .input-output-copier:hover { + background-color: #def; +} + +.problem-statement .test-example-line-even { + background-color: #E0E0E0; +} + +.statement-footnote { + font-size: 85%; + position: relative; +} + +.statement-footnote::before { + content: ""; + position: absolute; + top: -2px; + width: 25%; + border-top: 1px solid #888; +} + +.statement-footnote p { + margin-bottom: 0.5em; +} + +.statement-footnote p:last-child { + margin-bottom: 1em; +} + +.problem-statement .header .input-standard, +.problem-statement .header .output-standard { + display: none; +} diff --git a/exam-queue-17/statements/.html/russian/problem.html b/exam-queue-17/statements/.html/russian/problem.html new file mode 100644 index 0000000..dc1bbb0 --- /dev/null +++ b/exam-queue-17/statements/.html/russian/problem.html @@ -0,0 +1,29 @@ + + + + + + Очередь за кексами + + + +
Очередь за кексами
ограничение по времени на тест
1 секунда
ограничение по памяти на тест
256 мегабайт
ввод
стандартный ввод
вывод
стандартный вывод

В честь юбилея ректорат ЮФУ решил запустить акцию «Сто и десять кексов». В каждом корпусе университета открылась лавка с кексами, в которой каждый студент может получить бесплатные кексы.

Не прошло и пары минут после открытия, как к лавкам набежали студенты и образовалось много очередей. Но самая большая очередь образовалась в главном корпусе ЮФУ. Изначально в этой очереди стояло $$$n$$$ студентов, но потом в течение следующих $$$m$$$ минут какие-то студенты приходили и вставали в очередь, а какие-то уходили.

За каждым студентом закреплен номер его зачетной книжки, будем называть это число номером студента. У каждого студента будет уникальный номер, по которому можно однозначно его идентифицировать. Будем считать, что каждую минуту происходило одно из следующих событий:

  1. Студент с номером $$$x$$$ пришел и встал перед студентом с номером $$$y$$$;
  2. Студент с номером $$$x$$$ пришел и встал в конец очереди;
  3. Студент с номером $$$x$$$ ушел из очереди; возможно, он потом вернется.

Аналитикам стало интересно, а какой будет очередь после $$$m$$$ минут?

Помогите им и сообщите конечное состояние очереди.

Входные данные

В первой строке заданы два целых числа $$$n$$$ и $$$m$$$ $$$(1 \le n, m \le 10^5)$$$ — текущее число студентов в очереди и количество изменений.

В следующей строке задается $$$n$$$ целых различных чисел $$$a_1, a_2, \cdots , a_n$$$ $$$(1 \le a_i \le 10^9)$$$, где $$$a_i$$$ — номер студента, который стоит на $$$i$$$-й позиции в очереди.

В следующих $$$m$$$ строках идет описание запросов изменения очереди.

В каждой строке в зависимости от типа запроса задается два или три числа. Первое число $$$t_j$$$ $$$(1 \le t_j \le 3)$$$ — тип события, которое произошло в $$$j$$$-ю минуту.

Если $$$t_j = \textbf{1}$$$, то в строке задается еще 2 числа $$$x$$$ $$$(1 \le x_j \le 10^9)$$$ и $$$y$$$ $$$(1 \le y_j \le 10^9)$$$ — номер студента, который пришел, и номер студента, перед которым он встанет в очереди. Гарантируется, что студент с номером $$$x$$$ ещё не занял очередь, а студент с номером $$$y$$$ уже стоит в ней.

Если $$$t_j = \textbf{2}$$$, то в строке задается еще 1 число $$$x$$$ $$$(1 \le x_j \le 10^9)$$$ — номер студента, который пришел и встал в конец очереди. Гарантируется, что студент с номером $$$x$$$ ещё не занял очередь.

Если $$$t_j = \textbf{3}$$$, то в строке задается еще 1 число $$$x$$$ $$$(1 \le x_j \le 10^9)$$$ — номер студента, который ушел из очереди. Гарантируется, что студент с номером $$$x$$$ стоит в очереди.

Выходные данные

В первой строке выведите одно число $$$|a|$$$ — длину очереди после выполнения всех запросов изменения.

В следующей строке выведите $$$|a|$$$ чисел $$$a_1, a_2, \cdots , a_{|a|}$$$, где $$$a_i$$$ — номер студента, который стоит на $$$i$$$-й позиции в очереди.

Пример

Входные данные
+
7 6
1 2 3 4 5 6 7
1 8 3
2 9
3 3
1 3 9
2 10
3 1
Выходные данные
+9
+2 8 4 5 6 7 3 9 10 
+

Примечание

Изначально очередь выглядит следующим образом:

В первую минуту приходит студент с номером 8 и встает перед студентом с номером 3.

Потом студент с номером 9 встает в конец очереди.

Студент с номером 3 уходит из очереди.

Потом он возвращается и становится перед студентом с номером 9.

После в конец очереди становится студент с номером 10.

И студент с номером 1 уходит из очереди.

После $$$m$$$ событий очередь имеет следующий вид:

+ + \ No newline at end of file diff --git a/exam-queue-17/statements/.html/russian/tutorial.html b/exam-queue-17/statements/.html/russian/tutorial.html new file mode 100644 index 0000000..3df0336 --- /dev/null +++ b/exam-queue-17/statements/.html/russian/tutorial.html @@ -0,0 +1,17 @@ + + + + + + Очередь за кексами + + + +
Очередь за кексами

Давайте просто промоделируем все действия.

Заведем список элементов, а также сохраним по ключу $$$x$$$ указатель на элемент списка. Мы можем это сделать, так как все элементы различны. Например, в С++ можно просто завести коллекцию list<int>, а также map<int, list<int>::iterator> или реализовать свой список.

Теперь мы можем легко обрабатывать все запросы, а в конце просто выведем весь список.

Запрос 1-го типа можно обработать так: просто берем по ключу указатель на нужный элемент и вставляем перед ним другой элемент, останется только по ключу $$$x$$$ записать указатель на новый элемент.

Запрос 2-го типа — просто добавить в список элемент в конец и сохранить на него указатель.

Запрос 3-го типа — удаляем из списка элемент по его указателю.

В конце просто выводим массив.

Итоговая сложность $$$O(mlog(n))$$$

+ + \ No newline at end of file diff --git a/exam-queue-17/statements/.pdf/russian/problem.pdf b/exam-queue-17/statements/.pdf/russian/problem.pdf new file mode 100644 index 0000000..7c52e76 Binary files /dev/null and b/exam-queue-17/statements/.pdf/russian/problem.pdf differ diff --git a/exam-queue-17/statements/.pdf/russian/tutorial.pdf b/exam-queue-17/statements/.pdf/russian/tutorial.pdf new file mode 100644 index 0000000..06be88c Binary files /dev/null and b/exam-queue-17/statements/.pdf/russian/tutorial.pdf differ diff --git a/exam-queue-17/statements/russian/example.01 b/exam-queue-17/statements/russian/example.01 new file mode 100644 index 0000000..89161bd --- /dev/null +++ b/exam-queue-17/statements/russian/example.01 @@ -0,0 +1,8 @@ +7 6 +1 2 3 4 5 6 7 +1 8 3 +2 9 +3 3 +1 3 9 +2 10 +3 1 diff --git a/exam-queue-17/statements/russian/example.01.a b/exam-queue-17/statements/russian/example.01.a new file mode 100644 index 0000000..75d1da5 --- /dev/null +++ b/exam-queue-17/statements/russian/example.01.a @@ -0,0 +1,2 @@ +9 +2 8 4 5 6 7 3 9 10 diff --git a/exam-queue-17/statements/russian/example.01.mu b/exam-queue-17/statements/russian/example.01.mu new file mode 100644 index 0000000..830180e --- /dev/null +++ b/exam-queue-17/statements/russian/example.01.mu @@ -0,0 +1,8 @@ +MU7 6 +1 2 3 4 5 6 7 +1 8 3 +2 9 +3 3 +1 3 9 +2 10 +3 1 diff --git a/exam-queue-17/statements/russian/o1.png b/exam-queue-17/statements/russian/o1.png new file mode 100644 index 0000000..2b720a0 Binary files /dev/null and b/exam-queue-17/statements/russian/o1.png differ diff --git a/exam-queue-17/statements/russian/o2.png b/exam-queue-17/statements/russian/o2.png new file mode 100644 index 0000000..75a51e0 Binary files /dev/null and b/exam-queue-17/statements/russian/o2.png differ diff --git a/exam-queue-17/statements/russian/o3.png b/exam-queue-17/statements/russian/o3.png new file mode 100644 index 0000000..aed22b2 Binary files /dev/null and b/exam-queue-17/statements/russian/o3.png differ diff --git a/exam-queue-17/statements/russian/o4.png b/exam-queue-17/statements/russian/o4.png new file mode 100644 index 0000000..7774783 Binary files /dev/null and b/exam-queue-17/statements/russian/o4.png differ diff --git a/exam-queue-17/statements/russian/o5.png b/exam-queue-17/statements/russian/o5.png new file mode 100644 index 0000000..6fbbd47 Binary files /dev/null and b/exam-queue-17/statements/russian/o5.png differ diff --git a/exam-queue-17/statements/russian/o6.png b/exam-queue-17/statements/russian/o6.png new file mode 100644 index 0000000..b424e7a Binary files /dev/null and b/exam-queue-17/statements/russian/o6.png differ diff --git a/exam-queue-17/statements/russian/o7.png b/exam-queue-17/statements/russian/o7.png new file mode 100644 index 0000000..46ca714 Binary files /dev/null and b/exam-queue-17/statements/russian/o7.png differ diff --git a/exam-queue-17/statements/russian/o8.png b/exam-queue-17/statements/russian/o8.png new file mode 100644 index 0000000..9028df3 Binary files /dev/null and b/exam-queue-17/statements/russian/o8.png differ diff --git a/exam-queue-17/statements/russian/problem-properties.json b/exam-queue-17/statements/russian/problem-properties.json new file mode 100644 index 0000000..2a6a77d --- /dev/null +++ b/exam-queue-17/statements/russian/problem-properties.json @@ -0,0 +1 @@ +{"extraResources":{"example.01.mu":"TVXzATcgNg0KMSAyIDMgNCA1IDYgNw0KMSA4IDMNCjIgOQ0KMyAzDQoxIDMgOQ0KMiAxMA0KMyAxDQo="},"scoring":null,"notes":"Изначально очередь выглядит следующим образом:\r\n\r\n\\includegraphics{o1.png}\r\n\r\nВ первую минуту приходит студент с номером 8 и встает перед студентом с номером 3.\r\n\r\n\\includegraphics{o2.png}\r\n\r\nПотом студент с номером 9 встает в конец очереди.\r\n\r\n\\includegraphics{o3.png}\r\n\r\nСтудент с номером 3 уходит из очереди.\r\n\r\n\\includegraphics{o4.png}\r\n\r\nПотом он возвращается и становится перед студентом с номером 9.\r\n\r\n\\includegraphics{o5.png}\r\n\r\nПосле в конец очереди становится студент с номером 10.\r\n\r\n\\includegraphics{o6.png}\r\n\r\nИ студент с номером 1 уходит из очереди.\r\n\r\n\\includegraphics{o7.png}\r\n\r\nПосле $m$ событий очередь имеет следующий вид:\r\n\r\n\\includegraphics{o8.png}","legend":"В честь юбилея ректорат ЮФУ решил запустить акцию <<Сто и десять кексов>>. В каждом корпусе университета открылась лавка с кексами, в которой каждый студент может получить бесплатные кексы.\r\n\r\nНе прошло и пары минут после открытия, как к лавкам набежали студенты и образовалось много очередей. Но самая большая очередь образовалась в главном корпусе ЮФУ. Изначально в этой очереди стояло $n$ студентов, но потом в течение следующих $m$ минут какие-то студенты приходили и вставали в очередь, а какие-то уходили.\r\n\r\nЗа каждым студентом закреплен номер его зачетной книжки, будем называть это число номером студента. У каждого студента будет уникальный номер, по которому можно однозначно его идентифицировать. Будем считать, что каждую минуту происходило одно из следующих событий:\r\n\r\n\\begin{enumerate}\r\n \\item Студент с номером $x$ пришел и встал перед студентом с номером $y$;\r\n \\item Студент с номером $x$ пришел и встал в конец очереди;\r\n \\item Студент с номером $x$ ушел из очереди; возможно, он потом вернется.\r\n\\end{enumerate}\r\n\r\nАналитикам стало интересно, а какой будет очередь после $m$ минут? \r\n\r\nПомогите им и сообщите конечное состояние очереди.\r\n\r\n","authorLogin":"valavshonok","language":"russian","timeLimit":1000,"output":"В первой строке выведите одно число $|a|$~--- длину очереди после выполнения всех запросов изменения.\r\n\r\nВ следующей строке выведите $|a|$ чисел $a_1, a_2, \\cdots , a_{|a|}$, где $a_i$~--- номер студента, который стоит на $i$-й позиции в очереди.","inputFile":"stdin","outputFile":"stdout","input":"В первой строке заданы два целых числа $n$ и $m$ $(1 \\le n, m \\le 10^5)$~--- текущее число студентов в очереди и количество изменений.\r\n\r\nВ следующей строке задается $n$ целых \\textbf{различных} чисел $a_1, a_2, \\cdots , a_n$ $(1 \\le a_i \\le 10^9)$, где $a_i$~--- номер студента, который стоит на $i$-й позиции в очереди.\r\n\r\nВ следующих $m$ строках идет описание запросов изменения очереди.\r\n\r\nВ каждой строке в зависимости от типа запроса задается два или три числа. Первое число $t_j$ $(1 \\le t_j \\le 3)$~--- тип события, которое произошло в $j$-ю минуту.\r\n\r\nЕсли $t_j = \\textbf{1}$, то в строке задается еще 2 числа $x$ $(1 \\le x_j \\le 10^9)$ и $y$ $(1 \\le y_j \\le 10^9)$~--- номер студента, который пришел, и номер студента, перед которым он встанет в очереди. Гарантируется, что студент с номером $x$ ещё не занял очередь, а студент с номером $y$ уже стоит в ней. \r\n\r\nЕсли $t_j = \\textbf{2}$, то в строке задается еще 1 число $x$ $(1 \\le x_j \\le 10^9)$~--- номер студента, который пришел и встал в конец очереди. Гарантируется, что студент с номером $x$ ещё не занял очередь.\r\n\r\nЕсли $t_j = \\textbf{3}$, то в строке задается еще 1 число $x$ $(1 \\le x_j \\le 10^9)$~--- номер студента, который ушел из очереди. Гарантируется, что студент с номером $x$ стоит в очереди.","authorName":"Виталий Лавшонок","sampleTests":[{"output":"9\r\n2 8 4 5 6 7 3 9 10 \r\n","input":"7 6\r\n1 2 3 4 5 6 7\r\n1 8 3\r\n2 9\r\n3 3\r\n1 3 9\r\n2 10\r\n3 1\r\n","inputFile":"example.01","outputFile":"example.01.a"}],"name":"Очередь за кексами","interaction":null,"memoryLimit":268435456,"tutorial":"Давайте просто промоделируем все действия.\r\n\r\nЗаведем список элементов, а также сохраним по ключу $x$ указатель на элемент списка. Мы можем это сделать, так как все элементы различны. Например, в С++ можно просто завести коллекцию list, а также map::iterator> или реализовать свой список.\r\n\r\nТеперь мы можем легко обрабатывать все запросы, а в конце просто выведем весь список.\r\n\r\nЗапрос 1-го типа можно обработать так: просто берем по ключу указатель на нужный элемент и вставляем перед ним другой элемент, останется только по ключу $x$ записать указатель на новый элемент.\r\n\r\nЗапрос 2-го типа~--- просто добавить в список элемент в конец и сохранить на него указатель.\r\n\r\nЗапрос 3-го типа~--- удаляем из списка элемент по его указателю.\r\n\r\nВ конце просто выводим массив.\r\n\r\nИтоговая сложность $O(mlog(n))$"} \ No newline at end of file diff --git a/exam-queue-17/statements/russian/problem.tex b/exam-queue-17/statements/russian/problem.tex new file mode 100644 index 0000000..172dee5 --- /dev/null +++ b/exam-queue-17/statements/russian/problem.tex @@ -0,0 +1,81 @@ +\begin{problem}{Очередь за кексами}{стандартный ввод}{стандартный вывод}{1 секунда}{256 мегабайт} + +В честь юбилея ректорат ЮФУ решил запустить акцию <<Сто и десять кексов>>. В каждом корпусе университета открылась лавка с кексами, в которой каждый студент может получить бесплатные кексы. + +Не прошло и пары минут после открытия, как к лавкам набежали студенты и образовалось много очередей. Но самая большая очередь образовалась в главном корпусе ЮФУ. Изначально в этой очереди стояло $n$ студентов, но потом в течение следующих $m$ минут какие-то студенты приходили и вставали в очередь, а какие-то уходили. + +За каждым студентом закреплен номер его зачетной книжки, будем называть это число номером студента. У каждого студента будет уникальный номер, по которому можно однозначно его идентифицировать. Будем считать, что каждую минуту происходило одно из следующих событий: + +\begin{enumerate} + \item Студент с номером $x$ пришел и встал перед студентом с номером $y$; + \item Студент с номером $x$ пришел и встал в конец очереди; + \item Студент с номером $x$ ушел из очереди; возможно, он потом вернется. +\end{enumerate} + +Аналитикам стало интересно, а какой будет очередь после $m$ минут? + +Помогите им и сообщите конечное состояние очереди. + + + +\InputFile +В первой строке заданы два целых числа $n$ и $m$ $(1 \le n, m \le 10^5)$~--- текущее число студентов в очереди и количество изменений. + +В следующей строке задается $n$ целых \textbf{различных} чисел $a_1, a_2, \cdots , a_n$ $(1 \le a_i \le 10^9)$, где $a_i$~--- номер студента, который стоит на $i$-й позиции в очереди. + +В следующих $m$ строках идет описание запросов изменения очереди. + +В каждой строке в зависимости от типа запроса задается два или три числа. Первое число $t_j$ $(1 \le t_j \le 3)$~--- тип события, которое произошло в $j$-ю минуту. + +Если $t_j = \textbf{1}$, то в строке задается еще 2 числа $x$ $(1 \le x_j \le 10^9)$ и $y$ $(1 \le y_j \le 10^9)$~--- номер студента, который пришел, и номер студента, перед которым он встанет в очереди. Гарантируется, что студент с номером $x$ ещё не занял очередь, а студент с номером $y$ уже стоит в ней. + +Если $t_j = \textbf{2}$, то в строке задается еще 1 число $x$ $(1 \le x_j \le 10^9)$~--- номер студента, который пришел и встал в конец очереди. Гарантируется, что студент с номером $x$ ещё не занял очередь. + +Если $t_j = \textbf{3}$, то в строке задается еще 1 число $x$ $(1 \le x_j \le 10^9)$~--- номер студента, который ушел из очереди. Гарантируется, что студент с номером $x$ стоит в очереди. + +\OutputFile +В первой строке выведите одно число $|a|$~--- длину очереди после выполнения всех запросов изменения. + +В следующей строке выведите $|a|$ чисел $a_1, a_2, \cdots , a_{|a|}$, где $a_i$~--- номер студента, который стоит на $i$-й позиции в очереди. + +\Example + +\begin{example} +\exmpfile{example.01}{example.01.a}% +\end{example} + +\Note +Изначально очередь выглядит следующим образом: + +\includegraphics{o1.png} + +В первую минуту приходит студент с номером 8 и встает перед студентом с номером 3. + +\includegraphics{o2.png} + +Потом студент с номером 9 встает в конец очереди. + +\includegraphics{o3.png} + +Студент с номером 3 уходит из очереди. + +\includegraphics{o4.png} + +Потом он возвращается и становится перед студентом с номером 9. + +\includegraphics{o5.png} + +После в конец очереди становится студент с номером 10. + +\includegraphics{o6.png} + +И студент с номером 1 уходит из очереди. + +\includegraphics{o7.png} + +После $m$ событий очередь имеет следующий вид: + +\includegraphics{o8.png} + +\end{problem} + diff --git a/exam-queue-17/statements/russian/tutorial.tex b/exam-queue-17/statements/russian/tutorial.tex new file mode 100644 index 0000000..4032af4 --- /dev/null +++ b/exam-queue-17/statements/russian/tutorial.tex @@ -0,0 +1,19 @@ +\begin{tutorial}{Очередь за кексами} + +Давайте просто промоделируем все действия. + +Заведем список элементов, а также сохраним по ключу $x$ указатель на элемент списка. Мы можем это сделать, так как все элементы различны. Например, в С++ можно просто завести коллекцию list, а также map::iterator> или реализовать свой список. + +Теперь мы можем легко обрабатывать все запросы, а в конце просто выведем весь список. + +Запрос 1-го типа можно обработать так: просто берем по ключу указатель на нужный элемент и вставляем перед ним другой элемент, останется только по ключу $x$ записать указатель на новый элемент. + +Запрос 2-го типа~--- просто добавить в список элемент в конец и сохранить на него указатель. + +Запрос 3-го типа~--- удаляем из списка элемент по его указателю. + +В конце просто выводим массив. + +Итоговая сложность $O(mlog(n))$ + +\end{tutorial} diff --git a/exam-queue-17/tags b/exam-queue-17/tags new file mode 100644 index 0000000..9c33948 --- /dev/null +++ b/exam-queue-17/tags @@ -0,0 +1 @@ +realization diff --git a/exam-queue-17/tests/01 b/exam-queue-17/tests/01 new file mode 100644 index 0000000..89161bd --- /dev/null +++ b/exam-queue-17/tests/01 @@ -0,0 +1,8 @@ +7 6 +1 2 3 4 5 6 7 +1 8 3 +2 9 +3 3 +1 3 9 +2 10 +3 1 diff --git a/exam-queue-17/wipe.bat b/exam-queue-17/wipe.bat new file mode 100644 index 0000000..cfc277e --- /dev/null +++ b/exam-queue-17/wipe.bat @@ -0,0 +1,103 @@ +rem *** tests *** +del tests\01.a +del tests\02 +del tests\02.a +del tests\03 +del tests\03.a +del tests\04 +del tests\04.a +del tests\05 +del tests\05.a +del tests\06 +del tests\06.a +del tests\07 +del tests\07.a +del tests\08 +del tests\08.a +del tests\09 +del tests\09.a +del tests\10 +del tests\10.a +del tests\11 +del tests\11.a +del tests\12 +del tests\12.a +del tests\13 +del tests\13.a +del tests\14 +del tests\14.a +del tests\15 +del tests\15.a +del tests\16 +del tests\16.a +del tests\17 +del tests\17.a +del tests\18 +del tests\18.a +del tests\19 +del tests\19.a +del tests\20 +del tests\20.a +del tests\21 +del tests\21.a +del tests\22 +del tests\22.a +del tests\23 +del tests\23.a +del tests\24 +del tests\24.a +del tests\25 +del tests\25.a +del tests\26 +del tests\26.a +del tests\27 +del tests\27.a +del tests\28 +del tests\28.a +del tests\29 +del tests\29.a +del tests\30 +del tests\30.a +del tests\31 +del tests\31.a +del tests\32 +del tests\32.a +del tests\33 +del tests\33.a +del tests\34 +del tests\34.a +del tests\35 +del tests\35.a +del tests\36 +del tests\36.a +del tests\37 +del tests\37.a +del tests\38 +del tests\38.a +del tests\39 +del tests\39.a +del tests\40 +del tests\40.a +del tests\41 +del tests\41.a +del tests\42 +del tests\42.a +del tests\43 +del tests\43.a +del tests\44 +del tests\44.a +del tests\45 +del tests\45.a +del tests\46 +del tests\46.a +del tests\47 +del tests\47.a +del tests\48 +del tests\48.a +del tests\49 +del tests\49.a +del tests\50 +del tests\50.a +del tests\51 +del tests\51.a + diff --git a/exam-queue-17/wipe.sh b/exam-queue-17/wipe.sh new file mode 100644 index 0000000..71fb8e8 --- /dev/null +++ b/exam-queue-17/wipe.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# *** tests *** +rm -f tests/01.a +rm -f tests/02 +rm -f tests/02.a +rm -f tests/03 +rm -f tests/03.a +rm -f tests/04 +rm -f tests/04.a +rm -f tests/05 +rm -f tests/05.a +rm -f tests/06 +rm -f tests/06.a +rm -f tests/07 +rm -f tests/07.a +rm -f tests/08 +rm -f tests/08.a +rm -f tests/09 +rm -f tests/09.a +rm -f tests/10 +rm -f tests/10.a +rm -f tests/11 +rm -f tests/11.a +rm -f tests/12 +rm -f tests/12.a +rm -f tests/13 +rm -f tests/13.a +rm -f tests/14 +rm -f tests/14.a +rm -f tests/15 +rm -f tests/15.a +rm -f tests/16 +rm -f tests/16.a +rm -f tests/17 +rm -f tests/17.a +rm -f tests/18 +rm -f tests/18.a +rm -f tests/19 +rm -f tests/19.a +rm -f tests/20 +rm -f tests/20.a +rm -f tests/21 +rm -f tests/21.a +rm -f tests/22 +rm -f tests/22.a +rm -f tests/23 +rm -f tests/23.a +rm -f tests/24 +rm -f tests/24.a +rm -f tests/25 +rm -f tests/25.a +rm -f tests/26 +rm -f tests/26.a +rm -f tests/27 +rm -f tests/27.a +rm -f tests/28 +rm -f tests/28.a +rm -f tests/29 +rm -f tests/29.a +rm -f tests/30 +rm -f tests/30.a +rm -f tests/31 +rm -f tests/31.a +rm -f tests/32 +rm -f tests/32.a +rm -f tests/33 +rm -f tests/33.a +rm -f tests/34 +rm -f tests/34.a +rm -f tests/35 +rm -f tests/35.a +rm -f tests/36 +rm -f tests/36.a +rm -f tests/37 +rm -f tests/37.a +rm -f tests/38 +rm -f tests/38.a +rm -f tests/39 +rm -f tests/39.a +rm -f tests/40 +rm -f tests/40.a +rm -f tests/41 +rm -f tests/41.a +rm -f tests/42 +rm -f tests/42.a +rm -f tests/43 +rm -f tests/43.a +rm -f tests/44 +rm -f tests/44.a +rm -f tests/45 +rm -f tests/45.a +rm -f tests/46 +rm -f tests/46.a +rm -f tests/47 +rm -f tests/47.a +rm -f tests/48 +rm -f tests/48.a +rm -f tests/49 +rm -f tests/49.a +rm -f tests/50 +rm -f tests/50.a +rm -f tests/51 +rm -f tests/51.a + diff --git a/src/LiquidCode.Tester.Common/Models/ProblemPackage.cs b/src/LiquidCode.Tester.Common/Models/ProblemPackage.cs index e12297b..cf58d1d 100644 --- a/src/LiquidCode.Tester.Common/Models/ProblemPackage.cs +++ b/src/LiquidCode.Tester.Common/Models/ProblemPackage.cs @@ -3,6 +3,7 @@ namespace LiquidCode.Tester.Common.Models; public class ProblemPackage { public string WorkingDirectory { get; set; } = string.Empty; + public string? ExtractionRoot { get; set; } public List TestCases { get; set; } = new(); public string? CheckerPath { get; set; } public int DefaultTimeLimit { get; set; } = 2000; // milliseconds diff --git a/src/LiquidCode.Tester.Gateway/Controllers/TesterController.cs b/src/LiquidCode.Tester.Gateway/Controllers/TesterController.cs index 9b22213..ac52d66 100644 --- a/src/LiquidCode.Tester.Gateway/Controllers/TesterController.cs +++ b/src/LiquidCode.Tester.Gateway/Controllers/TesterController.cs @@ -30,11 +30,11 @@ public class TesterController : ControllerBase try { - // Download the package - var packagePath = await _packageDownloadService.DownloadPackageAsync(request.PackageUrl); + // Download the package or use cached version if available + var packagePath = await _packageDownloadService.GetOrDownloadPackageAsync(request.MissionId, request.PackageUrl); // Send to appropriate worker based on language - await _workerClientService.SendToWorkerAsync(request, packagePath); + await _workerClientService.SendToWorkerAsync(request, packagePath, deletePackageAfterSend: false); return Accepted(new { message = "Submit accepted for testing", submitId = request.Id }); } @@ -86,7 +86,7 @@ public class TesterController : ControllerBase ); // Send to appropriate worker based on language - await _workerClientService.SendToWorkerAsync(submitModel, packagePath); + await _workerClientService.SendToWorkerAsync(submitModel, packagePath, deletePackageAfterSend: true); return Accepted(new { message = "Submit accepted for testing", submitId = request.Id }); } diff --git a/src/LiquidCode.Tester.Gateway/Program.cs b/src/LiquidCode.Tester.Gateway/Program.cs index 4faf1c9..21263ab 100644 --- a/src/LiquidCode.Tester.Gateway/Program.cs +++ b/src/LiquidCode.Tester.Gateway/Program.cs @@ -10,6 +10,7 @@ builder.Services.AddOpenApi(); // Add HttpClient builder.Services.AddHttpClient(); +builder.Services.AddMemoryCache(); // Register application services builder.Services.AddSingleton(); diff --git a/src/LiquidCode.Tester.Gateway/Services/IPackageDownloadService.cs b/src/LiquidCode.Tester.Gateway/Services/IPackageDownloadService.cs index 1c23152..1b44d57 100644 --- a/src/LiquidCode.Tester.Gateway/Services/IPackageDownloadService.cs +++ b/src/LiquidCode.Tester.Gateway/Services/IPackageDownloadService.cs @@ -3,9 +3,10 @@ namespace LiquidCode.Tester.Gateway.Services; public interface IPackageDownloadService { /// - /// Downloads a package from the specified URL + /// Retrieves a cached package for the mission or downloads it if missing. /// - /// URL to download the package from - /// Path to the downloaded package file - Task DownloadPackageAsync(string packageUrl); + /// Unique mission identifier used as cache key. + /// URL to download the package from when cache is cold. + /// Path to the cached or downloaded package file. + Task GetOrDownloadPackageAsync(long missionId, string packageUrl); } diff --git a/src/LiquidCode.Tester.Gateway/Services/IWorkerClientService.cs b/src/LiquidCode.Tester.Gateway/Services/IWorkerClientService.cs index 2d49696..44aee88 100644 --- a/src/LiquidCode.Tester.Gateway/Services/IWorkerClientService.cs +++ b/src/LiquidCode.Tester.Gateway/Services/IWorkerClientService.cs @@ -8,6 +8,7 @@ public interface IWorkerClientService /// Sends a submit to the appropriate worker based on the language /// /// Submit data - /// Local path to the downloaded package - Task SendToWorkerAsync(SubmitForTesterModel submit, string packagePath); + /// Local path to the package that will be streamed to the worker + /// Indicates whether the package file should be removed after upload + Task SendToWorkerAsync(SubmitForTesterModel submit, string packagePath, bool deletePackageAfterSend); } diff --git a/src/LiquidCode.Tester.Gateway/Services/PackageDownloadService.cs b/src/LiquidCode.Tester.Gateway/Services/PackageDownloadService.cs index 5abab22..b636b84 100644 --- a/src/LiquidCode.Tester.Gateway/Services/PackageDownloadService.cs +++ b/src/LiquidCode.Tester.Gateway/Services/PackageDownloadService.cs @@ -1,3 +1,6 @@ +using System.Collections.Concurrent; +using Microsoft.Extensions.Caching.Memory; + namespace LiquidCode.Tester.Gateway.Services; public class PackageDownloadService : IPackageDownloadService @@ -5,14 +8,20 @@ public class PackageDownloadService : IPackageDownloadService private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger _logger; private readonly string _downloadDirectory; + private readonly IMemoryCache _memoryCache; + private readonly ConcurrentDictionary _locks = new(); + + private sealed record PackageCacheEntry(string FilePath); public PackageDownloadService( IHttpClientFactory httpClientFactory, ILogger logger, - IConfiguration configuration) + IConfiguration configuration, + IMemoryCache memoryCache) { _httpClientFactory = httpClientFactory; _logger = logger; + _memoryCache = memoryCache; _downloadDirectory = configuration["PackageDownloadDirectory"] ?? Path.Combine(Path.GetTempPath(), "packages"); if (!Directory.Exists(_downloadDirectory)) @@ -21,12 +30,25 @@ public class PackageDownloadService : IPackageDownloadService } } - public async Task DownloadPackageAsync(string packageUrl) + public async Task GetOrDownloadPackageAsync(long missionId, string packageUrl) { - _logger.LogInformation("Downloading package from {Url}", packageUrl); + if (TryGetCachedFile(missionId, out var cachedFile)) + { + return cachedFile; + } + var missionLock = _locks.GetOrAdd(missionId, _ => new SemaphoreSlim(1, 1)); + + await missionLock.WaitAsync(); try { + if (TryGetCachedFile(missionId, out cachedFile)) + { + return cachedFile; + } + + _logger.LogInformation("Downloading package for mission {MissionId} from {Url}", missionId, packageUrl); + var httpClient = _httpClientFactory.CreateClient(); var response = await httpClient.GetAsync(packageUrl); response.EnsureSuccessStatusCode(); @@ -34,16 +56,92 @@ public class PackageDownloadService : IPackageDownloadService var fileName = $"package_{Guid.NewGuid()}.zip"; var filePath = Path.Combine(_downloadDirectory, fileName); - await using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None); - await response.Content.CopyToAsync(fileStream); + try + { + await using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None); + await response.Content.CopyToAsync(fileStream); + } + catch + { + if (File.Exists(filePath)) + { + try + { + File.Delete(filePath); + } + catch (Exception cleanupEx) + { + _logger.LogWarning(cleanupEx, "Failed to clean up temporary file {Path} after download error", filePath); + } + } + + throw; + } + + CacheFile(missionId, filePath); + + _logger.LogInformation("Package downloaded and cached for mission {MissionId} at {Path}", missionId, filePath); - _logger.LogInformation("Package downloaded successfully to {Path}", filePath); return filePath; } catch (Exception ex) { - _logger.LogError(ex, "Failed to download package from {Url}", packageUrl); + _logger.LogError(ex, "Failed to download package for mission {MissionId} from {Url}", missionId, packageUrl); throw; } + finally + { + missionLock.Release(); + } + } + + private bool TryGetCachedFile(long missionId, out string filePath) + { + if (_memoryCache.TryGetValue(missionId, out PackageCacheEntry? cacheEntry)) + { + if (cacheEntry is not null && File.Exists(cacheEntry.FilePath)) + { + filePath = cacheEntry.FilePath; + _logger.LogInformation("Using cached package for mission {MissionId} from {Path}", missionId, filePath); + return true; + } + + _memoryCache.Remove(missionId); + } + + filePath = string.Empty; + return false; + } + + private void CacheFile(long missionId, string filePath) + { + var cacheEntry = new PackageCacheEntry(filePath); + var options = new MemoryCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) + }; + + options.RegisterPostEvictionCallback((_, value, _, _) => + { + if (value is not PackageCacheEntry entry) + { + return; + } + + try + { + if (File.Exists(entry.FilePath)) + { + File.Delete(entry.FilePath); + _logger.LogInformation("Removed cached package file {Path} after expiration", entry.FilePath); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to delete cached package file {Path} during eviction", entry.FilePath); + } + }); + + _memoryCache.Set(missionId, cacheEntry, options); } } diff --git a/src/LiquidCode.Tester.Gateway/Services/WorkerClientService.cs b/src/LiquidCode.Tester.Gateway/Services/WorkerClientService.cs index ca07a60..131d481 100644 --- a/src/LiquidCode.Tester.Gateway/Services/WorkerClientService.cs +++ b/src/LiquidCode.Tester.Gateway/Services/WorkerClientService.cs @@ -19,7 +19,7 @@ public class WorkerClientService : IWorkerClientService _configuration = configuration; } - public async Task SendToWorkerAsync(SubmitForTesterModel submit, string packagePath) + public async Task SendToWorkerAsync(SubmitForTesterModel submit, string packagePath, bool deletePackageAfterSend) { var workerUrl = GetWorkerUrlForLanguage(submit.Language); _logger.LogInformation("Sending submit {SubmitId} to worker at {WorkerUrl}", submit.Id, workerUrl); @@ -56,17 +56,20 @@ public class WorkerClientService : IWorkerClientService } finally { - // Clean up downloaded package - try + if (deletePackageAfterSend) { - if (File.Exists(packagePath)) + // Clean up package file when it is not needed anymore + try { - File.Delete(packagePath); + if (File.Exists(packagePath)) + { + File.Delete(packagePath); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to delete package file {Path}", packagePath); } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to delete package file {Path}", packagePath); } } } diff --git a/src/LiquidCode.Tester.Worker/Dockerfile b/src/LiquidCode.Tester.Worker/Dockerfile index f1f444f..d37ce2f 100644 --- a/src/LiquidCode.Tester.Worker/Dockerfile +++ b/src/LiquidCode.Tester.Worker/Dockerfile @@ -64,12 +64,45 @@ RUN apt-get update && \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* +# Install Isolate sandbox for secure code execution +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + git \ + libcap-dev \ + libsystemd-dev \ + pkg-config \ + && git clone https://github.com/ioi/isolate.git /tmp/isolate \ + && cd /tmp/isolate \ + && make isolate \ + && make install \ + && rm -rf /tmp/isolate \ + && apt-get remove -y git \ + && apt-get autoremove -y \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Create unprivileged user for running the worker service +RUN useradd -m -u 1001 -s /bin/bash workeruser && \ + mkdir -p /var/local/lib/isolate && \ + chmod 755 /var/local/lib/isolate && \ + chown -R workeruser:workeruser /var/local/lib/isolate + +# Configure isolate +RUN echo "cg_root = /sys/fs/cgroup" > /usr/local/etc/isolate && \ + echo "cg_enable = 1" >> /usr/local/etc/isolate && \ + echo "box_root = /var/local/lib/isolate" >> /usr/local/etc/isolate + # Copy published app COPY --from=publish /app/publish . -# Create temp directory for compilation and testing -RUN mkdir -p /tmp/testing +# Create temp directory for compilation and testing with proper permissions +RUN mkdir -p /tmp/testing && \ + chown -R workeruser:workeruser /tmp/testing && \ + chown -R workeruser:workeruser /app ENV ASPNETCORE_URLS=http://+:8080 +# Switch to unprivileged user +USER workeruser + ENTRYPOINT ["dotnet", "LiquidCode.Tester.Worker.dll"] diff --git a/src/LiquidCode.Tester.Worker/Program.cs b/src/LiquidCode.Tester.Worker/Program.cs index 0a8e4bf..5f43921 100644 --- a/src/LiquidCode.Tester.Worker/Program.cs +++ b/src/LiquidCode.Tester.Worker/Program.cs @@ -1,4 +1,5 @@ using LiquidCode.Tester.Worker.Services; +using LiquidCode.Tester.Worker.Services.Isolate; var builder = WebApplication.CreateBuilder(args); @@ -9,28 +10,37 @@ builder.Services.AddOpenApi(); // Add HttpClient builder.Services.AddHttpClient(); +// Register Isolate services +builder.Services.AddSingleton(); +builder.Services.AddSingleton(sp => +{ + var logger = sp.GetRequiredService>(); + var maxBoxes = builder.Configuration.GetValue("Isolate:MaxBoxes", 100); + return new IsolateBoxPool(maxBoxes, logger); +}); + // Register application services builder.Services.AddSingleton(); builder.Services.AddSingleton(); -builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); -// Register compilation services -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); +// Register Isolate compilation services +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); -// Register execution services -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); +// Register Isolate execution services +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); // Register testing service diff --git a/src/LiquidCode.Tester.Worker/Services/AnswerGenerationService.cs b/src/LiquidCode.Tester.Worker/Services/AnswerGenerationService.cs index e1440cc..a9c1095 100644 --- a/src/LiquidCode.Tester.Worker/Services/AnswerGenerationService.cs +++ b/src/LiquidCode.Tester.Worker/Services/AnswerGenerationService.cs @@ -136,23 +136,37 @@ public class AnswerGenerationService return (null, ""); } - if (solutionType.StartsWith("python.")) + 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) + var versionPart = solutionType.Replace("python", string.Empty, StringComparison.OrdinalIgnoreCase) + .Trim('.', ' '); + + if (string.IsNullOrWhiteSpace(versionPart)) + { + return ("python", "3"); + } + + // Normalize python version; Polygon often uses python.3 or python3.10 + versionPart = versionPart.TrimStart('.'); + + if (!versionPart.Contains('.')) + { + // Assume major version provided, default to CPython minor 10 + versionPart = versionPart switch + { + "2" => "2.7", + "3" => "3.10", + _ => $"3.{versionPart}" + }; + } + + return ("python", versionPart); } 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 + var standard = ExtractCppStandard(solutionType); + return ("cpp", standard); } if (solutionType.StartsWith("java")) @@ -178,4 +192,22 @@ public class AnswerGenerationService _logger.LogWarning("Unknown solution type: {Type}", solutionType); return (null, ""); } + + private static string ExtractCppStandard(string solutionType) + { + var knownStandards = new[] { "26", "23", "20", "17", "14", "11", "03", "98" }; + + foreach (var standard in knownStandards) + { + if (solutionType.Contains($"++{standard}", StringComparison.OrdinalIgnoreCase) || + solutionType.Contains($"c++{standard}", StringComparison.OrdinalIgnoreCase)) + { + // Normalize 03 to 03, 98 stays 98 + return standard.TrimStart('0'); + } + } + + // Default to modern standard if not specified + return "17"; + } } diff --git a/src/LiquidCode.Tester.Worker/Services/CSharpCompilationServiceIsolate.cs b/src/LiquidCode.Tester.Worker/Services/CSharpCompilationServiceIsolate.cs new file mode 100644 index 0000000..8f82480 --- /dev/null +++ b/src/LiquidCode.Tester.Worker/Services/CSharpCompilationServiceIsolate.cs @@ -0,0 +1,218 @@ +using LiquidCode.Tester.Worker.Services.Isolate; +using LiquidCode.Tester.Worker.Models; + +namespace LiquidCode.Tester.Worker.Services; + +/// +/// C# compilation service using Isolate sandbox for security +/// +public class CSharpCompilationServiceIsolate : ICompilationService +{ + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + private readonly IsolateService _isolateService; + private readonly IsolateBoxPool _boxPool; + + private const int CompilationTimeLimitSeconds = 30; + private const int CompilationMemoryLimitMb = 512; + + public CSharpCompilationServiceIsolate( + ILogger logger, + IConfiguration configuration, + IsolateService isolateService, + IsolateBoxPool boxPool) + { + _logger = logger; + _configuration = configuration; + _isolateService = isolateService; + _boxPool = boxPool; + } + + public async Task 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 with Isolate in {WorkingDirectory} with version {Version}", + workingDirectory, version ?? "latest"); + + try + { + await File.WriteAllTextAsync(sourceFilePath, sourceCode); + return await CompileFileInIsolateAsync(sourceFilePath, executablePath, version); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Isolate C# compilation"); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation error: {ex.Message}" + }; + } + } + + private async Task CompileFileInIsolateAsync( + string sourceFilePath, + string executablePath, + string? version = null) + { + int boxId = -1; + + try + { + boxId = await _boxPool.AcquireBoxAsync(); + _logger.LogDebug("Acquired isolate box {BoxId} for C# compilation", boxId); + + await _isolateService.InitBoxAsync(boxId); + + var boxDir = $"/var/local/lib/isolate/{boxId}/box"; + var sourceFileName = Path.GetFileName(sourceFilePath); + var boxSourcePath = Path.Combine(boxDir, sourceFileName); + var exeFileName = "solution.exe"; + var boxExePath = Path.Combine(boxDir, exeFileName); + + File.Copy(sourceFilePath, boxSourcePath, overwrite: true); + + var (compiler, compilerFlags) = ResolveVersion(version); + _logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, compilerFlags); + + var arguments = new List(); + if (!string.IsNullOrWhiteSpace(compilerFlags)) + { + arguments.AddRange(compilerFlags.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + } + arguments.Add($"/out:/box/{exeFileName}"); + arguments.Add($"/box/{sourceFileName}"); + + var stderrFilePath = Path.Combine(boxDir, "compile_stderr.txt"); + + var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions + { + BoxId = boxId, + Executable = $"/usr/bin/{compiler}", + Arguments = arguments.ToArray(), + TimeLimitSeconds = CompilationTimeLimitSeconds, + WallTimeLimitSeconds = CompilationTimeLimitSeconds * 2, + MemoryLimitKb = CompilationMemoryLimitMb * 1024, + StackLimitKb = 256 * 1024, + ProcessLimit = 10, + EnableNetwork = false, + StderrFile = stderrFilePath, + WorkingDirectory = "/box", + DirectoryBindings = new List + { + new DirectoryBinding { HostPath = "/usr/lib", SandboxPath = "/usr/lib", ReadOnly = true }, + new DirectoryBinding { HostPath = "/lib", SandboxPath = "/lib", ReadOnly = true } + } + }); + + var compilerOutput = string.Empty; + if (File.Exists(stderrFilePath)) + { + compilerOutput = await File.ReadAllTextAsync(stderrFilePath); + } + if (!string.IsNullOrEmpty(isolateResult.ErrorOutput)) + { + compilerOutput = $"{compilerOutput}\n{isolateResult.ErrorOutput}".Trim(); + } + + if (isolateResult.TimeLimitExceeded) + { + _logger.LogWarning("C# compilation time limit exceeded for box {BoxId}", boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation timeout: exceeded {CompilationTimeLimitSeconds}s limit", + CompilerOutput = compilerOutput + }; + } + + if (isolateResult.MemoryLimitExceeded) + { + _logger.LogWarning("C# compilation memory limit exceeded for box {BoxId}", boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation memory limit exceeded: {CompilationMemoryLimitMb}MB", + CompilerOutput = compilerOutput + }; + } + + if (isolateResult.ExitCode == 0 && File.Exists(boxExePath)) + { + Directory.CreateDirectory(Path.GetDirectoryName(executablePath)!); + File.Copy(boxExePath, executablePath, overwrite: true); + + _logger.LogInformation("C# compilation successful in Isolate box {BoxId}", boxId); + return new CompilationResult + { + Success = true, + ExecutablePath = executablePath, + CompilerOutput = compilerOutput + }; + } + + _logger.LogWarning("C# compilation failed with exit code {ExitCode} in box {BoxId}", + isolateResult.ExitCode, boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation failed with exit code {isolateResult.ExitCode}", + CompilerOutput = compilerOutput + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Isolate C# compilation"); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation error: {ex.Message}" + }; + } + finally + { + if (boxId >= 0) + { + try + { + await _isolateService.CleanupBoxAsync(boxId); + _logger.LogDebug("Cleaned up isolate box {BoxId}", boxId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to cleanup compilation box {BoxId}", boxId); + } + + _boxPool.ReleaseBox(boxId); + _logger.LogDebug("Released compilation box {BoxId} back to pool", boxId); + } + } + } + + private (string compiler, string compilerFlags) ResolveVersion(string? version) + { + if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase)) + { + var compiler = _configuration["CSharp:Compiler"] ?? "csc"; + var compilerFlags = _configuration["CSharp:CompilerFlags"] ?? "/optimize+"; + return (compiler, compilerFlags); + } + + 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+"); + } + + _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); + } +} diff --git a/src/LiquidCode.Tester.Worker/Services/CSharpExecutionServiceIsolate.cs b/src/LiquidCode.Tester.Worker/Services/CSharpExecutionServiceIsolate.cs new file mode 100644 index 0000000..709d7be --- /dev/null +++ b/src/LiquidCode.Tester.Worker/Services/CSharpExecutionServiceIsolate.cs @@ -0,0 +1,175 @@ +using System.Diagnostics; +using LiquidCode.Tester.Worker.Services.Isolate; + +namespace LiquidCode.Tester.Worker.Services; + +/// +/// C# program execution service using Isolate sandbox +/// +public class CSharpExecutionServiceIsolate : IExecutionService +{ + private readonly ILogger _logger; + private readonly IsolateService _isolateService; + private readonly IsolateBoxPool _boxPool; + + public CSharpExecutionServiceIsolate( + ILogger logger, + IsolateService isolateService, + IsolateBoxPool boxPool) + { + _logger = logger; + _isolateService = isolateService; + _boxPool = boxPool; + } + + public async Task ExecuteAsync( + string executablePath, + string inputFilePath, + int timeLimitMs, + int memoryLimitMb) + { + _logger.LogInformation( + "Executing C# with Isolate: {Executable}, time={TimeLimit}ms, memory={MemoryLimit}MB", + executablePath, timeLimitMs, memoryLimitMb); + + var result = new ExecutionResult(); + var stopwatch = Stopwatch.StartNew(); + int boxId = -1; + + try + { + // Acquire a box from pool + boxId = await _boxPool.AcquireBoxAsync(); + _logger.LogDebug("Acquired isolate box {BoxId}", boxId); + + // Initialize the box + await _isolateService.InitBoxAsync(boxId); + + // Copy executable to box + var boxDir = $"/var/local/lib/isolate/{boxId}/box"; + var executableName = Path.GetFileName(executablePath); + var boxExecutablePath = Path.Combine(boxDir, executableName); + + File.Copy(executablePath, boxExecutablePath, overwrite: true); + + // Make executable + var chmodProcess = Process.Start(new ProcessStartInfo + { + FileName = "chmod", + Arguments = $"+x {boxExecutablePath}", + UseShellExecute = false + }); + chmodProcess?.WaitForExit(); + + // Prepare output file in box + var outputFilePath = Path.Combine(boxDir, "output.txt"); + + // Run in Isolate + var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions + { + BoxId = boxId, + Executable = $"/box/{executableName}", + TimeLimitSeconds = timeLimitMs / 1000.0, + WallTimeLimitSeconds = (timeLimitMs / 1000.0) * 2, + MemoryLimitKb = memoryLimitMb * 1024, + StackLimitKb = 256 * 1024, + ProcessLimit = 1, // Single process for C# + EnableNetwork = false, + StdinFile = inputFilePath, + StdoutFile = outputFilePath, + WorkingDirectory = "/box" + }); + + stopwatch.Stop(); + + // Read output + if (File.Exists(outputFilePath)) + { + result.Output = await File.ReadAllTextAsync(outputFilePath); + } + + // Map Isolate result to ExecutionResult + result.ExecutionTimeMs = (long)(isolateResult.CpuTimeSeconds * 1000); + result.MemoryUsedMb = isolateResult.MemoryUsedKb / 1024; + result.ErrorOutput = isolateResult.ErrorOutput; + result.ExitCode = isolateResult.ExitCode; + + if (isolateResult.TimeLimitExceeded) + { + result.TimeLimitExceeded = true; + result.ErrorMessage = $"Time limit exceeded: {isolateResult.CpuTimeSeconds:F3}s"; + _logger.LogWarning("Time limit exceeded for box {BoxId}", boxId); + } + else if (isolateResult.MemoryLimitExceeded) + { + result.MemoryLimitExceeded = true; + result.ErrorMessage = $"Memory limit exceeded: {isolateResult.MemoryUsedKb / 1024}MB"; + if (isolateResult.CgroupOomKilled) + { + result.ErrorMessage += " (OOM killed by cgroup)"; + } + _logger.LogWarning("Memory limit exceeded for box {BoxId}", boxId); + } + else if (isolateResult.RuntimeError) + { + result.RuntimeError = true; + result.ErrorMessage = $"Runtime error: {isolateResult.Message}"; + if (isolateResult.ExitSignal.HasValue) + { + result.ErrorMessage += $" (signal {isolateResult.ExitSignal})"; + } + _logger.LogWarning("Runtime error for box {BoxId}: {Message}", boxId, isolateResult.Message); + } + else if (isolateResult.ExitCode == 0) + { + result.Success = true; + _logger.LogInformation( + "Execution successful: time={Time}ms, memory={Memory}MB, box={BoxId}", + result.ExecutionTimeMs, result.MemoryUsedMb, boxId); + } + else + { + result.RuntimeError = true; + result.ErrorMessage = $"Non-zero exit code: {isolateResult.ExitCode}"; + _logger.LogWarning("Non-zero exit code {ExitCode} for box {BoxId}", + isolateResult.ExitCode, boxId); + } + + _logger.LogDebug( + "Isolate stats: CPU={Cpu}s, Wall={Wall}s, Memory={Mem}KB", + isolateResult.CpuTimeSeconds, + isolateResult.WallTimeSeconds, + isolateResult.MemoryUsedKb); + } + catch (Exception ex) + { + stopwatch.Stop(); + result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds; + result.RuntimeError = true; + result.ErrorMessage = $"Execution error: {ex.Message}"; + _logger.LogError(ex, "Error during Isolate execution"); + } + finally + { + // Cleanup box + if (boxId >= 0) + { + try + { + await _isolateService.CleanupBoxAsync(boxId); + _logger.LogDebug("Cleaned up isolate box {BoxId}", boxId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to cleanup box {BoxId}", boxId); + } + + // Release box back to pool + _boxPool.ReleaseBox(boxId); + _logger.LogDebug("Released box {BoxId} back to pool", boxId); + } + } + + return result; + } +} diff --git a/src/LiquidCode.Tester.Worker/Services/CheckerServiceIsolate.cs b/src/LiquidCode.Tester.Worker/Services/CheckerServiceIsolate.cs new file mode 100644 index 0000000..b774bb8 --- /dev/null +++ b/src/LiquidCode.Tester.Worker/Services/CheckerServiceIsolate.cs @@ -0,0 +1,222 @@ +using LiquidCode.Tester.Worker.Services.Isolate; + +namespace LiquidCode.Tester.Worker.Services; + +/// +/// Service for running custom checkers in Isolate sandbox +/// +public class CheckerServiceIsolate +{ + private readonly ILogger _logger; + private readonly IsolateService _isolateService; + private readonly IsolateBoxPool _boxPool; + + private const int CheckerTimeLimitSeconds = 5; + private const int CheckerMemoryLimitMb = 256; + + public CheckerServiceIsolate( + ILogger logger, + IsolateService isolateService, + IsolateBoxPool boxPool) + { + _logger = logger; + _isolateService = isolateService; + _boxPool = boxPool; + } + + /// + /// Check user output using custom checker in Isolate sandbox + /// + /// Path to checker executable + /// Path to input file + /// User program output + /// Path to answer file + /// Checker result + public async Task 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" + }; + } + + int boxId = -1; + + try + { + boxId = await _boxPool.AcquireBoxAsync(); + _logger.LogDebug("Acquired isolate box {BoxId} for checker execution", boxId); + + await _isolateService.InitBoxAsync(boxId); + + var boxDir = $"/var/local/lib/isolate/{boxId}/box"; + + // Copy checker executable to box + var checkerName = Path.GetFileName(checkerPath); + var boxCheckerPath = Path.Combine(boxDir, checkerName); + File.Copy(checkerPath, boxCheckerPath, overwrite: true); + + // Make checker executable + var chmodProcess = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = "chmod", + Arguments = $"+x {boxCheckerPath}", + UseShellExecute = false + }); + chmodProcess?.WaitForExit(); + + // Copy input file + var inputName = "input.txt"; + var boxInputPath = Path.Combine(boxDir, inputName); + File.Copy(inputPath, boxInputPath, overwrite: true); + + // Save user output to file in box + var outputName = "output.txt"; + var boxOutputPath = Path.Combine(boxDir, outputName); + await File.WriteAllTextAsync(boxOutputPath, userOutput); + + // Copy answer file + var answerName = "answer.txt"; + var boxAnswerPath = Path.Combine(boxDir, answerName); + File.Copy(answerPath, boxAnswerPath, overwrite: true); + + // Prepare files for checker stdout/stderr + var stdoutPath = Path.Combine(boxDir, "checker_stdout.txt"); + var stderrPath = Path.Combine(boxDir, "checker_stderr.txt"); + + _logger.LogDebug("Running checker in Isolate: {Checker} {Input} {Output} {Answer}", + checkerName, inputName, outputName, answerName); + + // Run checker in Isolate + // Checker arguments: + var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions + { + BoxId = boxId, + Executable = $"/box/{checkerName}", + Arguments = new[] { $"/box/{inputName}", $"/box/{outputName}", $"/box/{answerName}" }, + TimeLimitSeconds = CheckerTimeLimitSeconds, + WallTimeLimitSeconds = CheckerTimeLimitSeconds * 2, + MemoryLimitKb = CheckerMemoryLimitMb * 1024, + StackLimitKb = 64 * 1024, + ProcessLimit = 1, // Single process for checker + EnableNetwork = false, + StdoutFile = stdoutPath, + StderrFile = stderrPath, + WorkingDirectory = "/box" + }); + + // Read checker output + var stdout = string.Empty; + if (File.Exists(stdoutPath)) + { + stdout = await File.ReadAllTextAsync(stdoutPath); + } + + var stderr = string.Empty; + if (File.Exists(stderrPath)) + { + stderr = await File.ReadAllTextAsync(stderrPath); + } + + if (isolateResult.TimeLimitExceeded) + { + _logger.LogWarning("Checker timeout in box {BoxId}", boxId); + return new CheckerResult + { + Accepted = false, + ExitCode = -1, + Message = "Checker timeout", + Verdict = CheckerVerdict.CheckerFailed + }; + } + + if (isolateResult.MemoryLimitExceeded) + { + _logger.LogWarning("Checker memory limit exceeded in box {BoxId}", boxId); + return new CheckerResult + { + Accepted = false, + ExitCode = -1, + Message = "Checker memory limit exceeded", + Verdict = CheckerVerdict.CheckerFailed + }; + } + + if (isolateResult.RuntimeError) + { + _logger.LogWarning("Checker runtime error in box {BoxId}: {Message}", boxId, isolateResult.Message); + return new CheckerResult + { + Accepted = false, + ExitCode = isolateResult.ExitCode, + Message = $"Checker runtime error: {isolateResult.Message}", + Verdict = CheckerVerdict.CheckerFailed + }; + } + + var exitCode = isolateResult.ExitCode; + var message = string.IsNullOrWhiteSpace(stderr) ? stdout : stderr; + + _logger.LogDebug("Checker exit code: {ExitCode}, message: {Message}", exitCode, message); + + return new CheckerResult + { + Accepted = exitCode == 0, + ExitCode = exitCode, + Message = message, + Verdict = GetVerdictFromExitCode(exitCode) + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error running checker in Isolate"); + return new CheckerResult + { + Accepted = false, + ExitCode = -1, + Message = $"Checker error: {ex.Message}", + Verdict = CheckerVerdict.CheckerFailed + }; + } + finally + { + if (boxId >= 0) + { + try + { + await _isolateService.CleanupBoxAsync(boxId); + _logger.LogDebug("Cleaned up isolate box {BoxId}", boxId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to cleanup checker box {BoxId}", boxId); + } + + _boxPool.ReleaseBox(boxId); + _logger.LogDebug("Released checker box {BoxId} back to pool", boxId); + } + } + } + + 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 + }; + } +} diff --git a/src/LiquidCode.Tester.Worker/Services/CompilationServiceFactory.cs b/src/LiquidCode.Tester.Worker/Services/CompilationServiceFactory.cs index f502384..6546437 100644 --- a/src/LiquidCode.Tester.Worker/Services/CompilationServiceFactory.cs +++ b/src/LiquidCode.Tester.Worker/Services/CompilationServiceFactory.cs @@ -1,29 +1,36 @@ namespace LiquidCode.Tester.Worker.Services; +/// +/// Factory for compilation services - always uses Isolate sandbox for security +/// public class CompilationServiceFactory : ICompilationServiceFactory { private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; - public CompilationServiceFactory(IServiceProvider serviceProvider, ILogger logger) + public CompilationServiceFactory( + IServiceProvider serviceProvider, + ILogger logger) { _serviceProvider = serviceProvider; _logger = logger; + + _logger.LogInformation("Compilation services configured to use Isolate sandbox"); } public ICompilationService GetCompilationService(string language) { var normalizedLanguage = language.ToLowerInvariant().Replace(" ", ""); - _logger.LogInformation("Getting compilation service for language: {Language}", normalizedLanguage); + _logger.LogDebug("Getting Isolate compilation service for language: {Language}", normalizedLanguage); return normalizedLanguage switch { - "c++" or "cpp" => _serviceProvider.GetRequiredService(), - "java" => _serviceProvider.GetRequiredService(), - "kotlin" => _serviceProvider.GetRequiredService(), - "c#" or "csharp" => _serviceProvider.GetRequiredService(), - "python" => _serviceProvider.GetRequiredService(), + "c++" or "cpp" => _serviceProvider.GetRequiredService(), + "java" => _serviceProvider.GetRequiredService(), + "kotlin" => _serviceProvider.GetRequiredService(), + "c#" or "csharp" => _serviceProvider.GetRequiredService(), + "python" => _serviceProvider.GetRequiredService(), _ => throw new NotSupportedException($"Language '{language}' is not supported") }; } diff --git a/src/LiquidCode.Tester.Worker/Services/CppCompilationService.cs b/src/LiquidCode.Tester.Worker/Services/CppCompilationService.cs index b076563..8472181 100644 --- a/src/LiquidCode.Tester.Worker/Services/CppCompilationService.cs +++ b/src/LiquidCode.Tester.Worker/Services/CppCompilationService.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using LiquidCode.Tester.Worker.Models; namespace LiquidCode.Tester.Worker.Services; @@ -23,57 +25,8 @@ public class CppCompilationService : ICompilationService try { - // Write source code to file 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 process = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = compiler, - Arguments = $"{compilerFlags} {sourceFilePath} -o {executablePath}", - 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("Compilation successful"); - return new CompilationResult - { - Success = true, - ExecutablePath = executablePath, - CompilerOutput = compilerOutput - }; - } - else - { - _logger.LogWarning("Compilation failed with exit code {ExitCode}", process.ExitCode); - return new CompilationResult - { - Success = false, - ErrorMessage = "Compilation failed", - CompilerOutput = compilerOutput - }; - } + return await CompileFileAsync(sourceFilePath, executablePath, version); } catch (Exception ex) { @@ -86,31 +39,168 @@ public class CppCompilationService : ICompilationService } } - private (string compiler, string compilerFlags) ResolveVersion(string? version) + public async Task CompileFileAsync( + string sourceFilePath, + string outputExecutablePath, + string? version = null, + IEnumerable? includeDirectories = null, + IEnumerable? additionalFlags = null) { - // If version is null or "latest", use default configuration + _logger.LogInformation("Compiling C++ source {Source} -> {Output} with version {Version}", sourceFilePath, outputExecutablePath, version ?? "latest"); + + try + { + Directory.CreateDirectory(Path.GetDirectoryName(outputExecutablePath)!); + + var (compiler, compilerFlags) = ResolveVersion(version); + + _logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, string.Join(' ', compilerFlags)); + + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = compiler, + WorkingDirectory = Path.GetDirectoryName(sourceFilePath) ?? Directory.GetCurrentDirectory(), + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + foreach (var flag in compilerFlags) + { + process.StartInfo.ArgumentList.Add(flag); + } + + if (includeDirectories != null) + { + foreach (var includeDir in includeDirectories.Where(d => !string.IsNullOrWhiteSpace(d))) + { + process.StartInfo.ArgumentList.Add($"-I{includeDir}"); + } + } + + if (additionalFlags != null) + { + foreach (var flag in additionalFlags.Where(f => !string.IsNullOrWhiteSpace(f))) + { + process.StartInfo.ArgumentList.Add(flag); + } + } + + process.StartInfo.ArgumentList.Add(sourceFilePath); + process.StartInfo.ArgumentList.Add("-o"); + process.StartInfo.ArgumentList.Add(outputExecutablePath); + + process.Start(); + + var stdOutTask = process.StandardOutput.ReadToEndAsync(); + var stdErrTask = process.StandardError.ReadToEndAsync(); + + await process.WaitForExitAsync(); + + var compilerOutput = $"{await stdOutTask}\n{await stdErrTask}".Trim(); + + if (process.ExitCode == 0 && File.Exists(outputExecutablePath)) + { + _logger.LogInformation("Compilation successful"); + return new CompilationResult + { + Success = true, + ExecutablePath = outputExecutablePath, + CompilerOutput = compilerOutput + }; + } + + _logger.LogWarning("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 compilation"); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation error: {ex.Message}" + }; + } + } + + private (string compiler, List compilerFlags) ResolveVersion(string? version) + { + var defaultCompiler = _configuration["Cpp:Compiler"] ?? "g++"; + var defaultFlags = SplitFlags(_configuration["Cpp:CompilerFlags"] ?? "-O2 -std=c++17 -Wall"); + 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); + return (defaultCompiler, defaultFlags); } - // Try to find version-specific configuration var versionKey = $"Cpp:Versions:{version}"; var versionCompiler = _configuration[$"{versionKey}:Compiler"]; - var versionFlags = _configuration[$"{versionKey}:CompilerFlags"]; + var versionFlagsValue = _configuration[$"{versionKey}:CompilerFlags"]; - if (!string.IsNullOrEmpty(versionCompiler)) + if (!string.IsNullOrEmpty(versionCompiler) || !string.IsNullOrEmpty(versionFlagsValue)) { + var resolvedFlags = !string.IsNullOrEmpty(versionFlagsValue) + ? SplitFlags(versionFlagsValue) + : defaultFlags; + _logger.LogInformation("Using C++ version {Version} configuration", version); - return (versionCompiler, versionFlags ?? "-O2 -Wall"); + return (versionCompiler ?? defaultCompiler, resolvedFlags); + } + + var normalized = NormalizeCppVersion(version); + if (normalized != null) + { + var flagsWithoutStd = defaultFlags + .Where(flag => !flag.StartsWith("-std=", StringComparison.OrdinalIgnoreCase)) + .ToList(); + flagsWithoutStd.Add($"-std=c++{normalized}"); + _logger.LogInformation("Using inferred C++ standard c++{Standard}", normalized); + return (defaultCompiler, flagsWithoutStd); } - // 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); } + + private static List SplitFlags(string flags) => + flags.Split(' ', StringSplitOptions.RemoveEmptyEntries) + .ToList(); + + private static string? NormalizeCppVersion(string version) + { + var cleaned = version.Trim().ToLowerInvariant(); + cleaned = cleaned.Replace("c++", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace("gnu++", string.Empty, StringComparison.OrdinalIgnoreCase) + .Trim('+', ' '); + + cleaned = cleaned switch + { + "2b" => "23", + "2a" => "20", + "1z" => "17", + "0x" => "11", + _ => cleaned + }; + + return cleaned switch + { + "26" => "26", + "23" => "23", + "20" => "20", + "17" => "17", + "14" => "14", + "11" => "11", + _ => null + }; + } } diff --git a/src/LiquidCode.Tester.Worker/Services/CppCompilationServiceIsolate.cs b/src/LiquidCode.Tester.Worker/Services/CppCompilationServiceIsolate.cs new file mode 100644 index 0000000..ef2d694 --- /dev/null +++ b/src/LiquidCode.Tester.Worker/Services/CppCompilationServiceIsolate.cs @@ -0,0 +1,304 @@ +using System.Collections.Generic; +using System.Linq; +using LiquidCode.Tester.Worker.Services.Isolate; +using LiquidCode.Tester.Worker.Models; + +namespace LiquidCode.Tester.Worker.Services; + +/// +/// C++ compilation service using Isolate sandbox for security +/// +public class CppCompilationServiceIsolate : ICompilationService +{ + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + private readonly IsolateService _isolateService; + private readonly IsolateBoxPool _boxPool; + + // Compilation limits (more generous than execution limits) + private const int CompilationTimeLimitSeconds = 30; + private const int CompilationMemoryLimitMb = 512; + + public CppCompilationServiceIsolate( + ILogger logger, + IConfiguration configuration, + IsolateService isolateService, + IsolateBoxPool boxPool) + { + _logger = logger; + _configuration = configuration; + _isolateService = isolateService; + _boxPool = boxPool; + } + + public async Task CompileAsync(string sourceCode, string workingDirectory, string? version = null) + { + var sourceFilePath = Path.Combine(workingDirectory, "solution.cpp"); + var executablePath = Path.Combine(workingDirectory, "solution"); + + _logger.LogInformation("Compiling C++ code with Isolate in {WorkingDirectory} with version {Version}", + workingDirectory, version ?? "latest"); + + try + { + await File.WriteAllTextAsync(sourceFilePath, sourceCode); + return await CompileFileAsync(sourceFilePath, executablePath, version); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Isolate compilation"); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation error: {ex.Message}" + }; + } + } + + /// + /// Compile a C++ source file to an executable using Isolate sandbox + /// + public async Task CompileFileAsync( + string sourceFilePath, + string outputExecutablePath, + string? version = null, + IEnumerable? includeDirectories = null, + IEnumerable? additionalFlags = null) + { + int boxId = -1; + + try + { + // Acquire box from pool + boxId = await _boxPool.AcquireBoxAsync(); + _logger.LogDebug("Acquired isolate box {BoxId} for compilation", boxId); + + // Initialize box + await _isolateService.InitBoxAsync(boxId); + + // Copy source file to box + var boxDir = $"/var/local/lib/isolate/{boxId}/box"; + var sourceFileName = Path.GetFileName(sourceFilePath); + var boxSourcePath = Path.Combine(boxDir, sourceFileName); + var outputFileName = Path.GetFileName(outputExecutablePath); + var boxOutputPath = Path.Combine(boxDir, outputFileName); + + File.Copy(sourceFilePath, boxSourcePath, overwrite: true); + + // Resolve compiler and flags + var (compiler, compilerFlags) = ResolveVersion(version); + _logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, string.Join(' ', compilerFlags)); + + // Build compiler arguments + var arguments = new List(compilerFlags); + + // Add include directories + if (includeDirectories != null) + { + foreach (var includeDir in includeDirectories.Where(d => !string.IsNullOrWhiteSpace(d))) + { + arguments.Add($"-I{includeDir}"); + } + } + + // Add additional flags + if (additionalFlags != null) + { + foreach (var flag in additionalFlags.Where(f => !string.IsNullOrWhiteSpace(f))) + { + arguments.Add(flag); + } + } + + arguments.Add($"/box/{sourceFileName}"); + arguments.Add("-o"); + arguments.Add($"/box/{outputFileName}"); + + // Prepare stderr output file for compiler messages + var stderrFilePath = Path.Combine(boxDir, "compile_stderr.txt"); + + // Run compiler in Isolate + // Note: Isolate by default provides access to /usr, /lib, etc. via --share-net=no + // For compilation, we need access to system headers and libraries + var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions + { + BoxId = boxId, + Executable = $"/usr/bin/{compiler}", + Arguments = arguments.ToArray(), + TimeLimitSeconds = CompilationTimeLimitSeconds, + WallTimeLimitSeconds = CompilationTimeLimitSeconds * 2, + MemoryLimitKb = CompilationMemoryLimitMb * 1024, + StackLimitKb = 256 * 1024, + ProcessLimit = 10, // g++ spawns multiple processes + EnableNetwork = false, + StderrFile = stderrFilePath, + WorkingDirectory = "/box", + DirectoryBindings = new List + { + new DirectoryBinding { HostPath = "/usr/include", SandboxPath = "/usr/include", ReadOnly = true }, + new DirectoryBinding { HostPath = "/usr/lib", SandboxPath = "/usr/lib", ReadOnly = true }, + new DirectoryBinding { HostPath = "/lib", SandboxPath = "/lib", ReadOnly = true } + } + }); + + // Read compiler output + var compilerOutput = string.Empty; + if (File.Exists(stderrFilePath)) + { + compilerOutput = await File.ReadAllTextAsync(stderrFilePath); + } + if (!string.IsNullOrEmpty(isolateResult.ErrorOutput)) + { + compilerOutput = $"{compilerOutput}\n{isolateResult.ErrorOutput}".Trim(); + } + + // Check for time/memory limits during compilation + if (isolateResult.TimeLimitExceeded) + { + _logger.LogWarning("Compilation time limit exceeded for box {BoxId}", boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation timeout: exceeded {CompilationTimeLimitSeconds}s limit", + CompilerOutput = compilerOutput + }; + } + + if (isolateResult.MemoryLimitExceeded) + { + _logger.LogWarning("Compilation memory limit exceeded for box {BoxId}", boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation memory limit exceeded: {CompilationMemoryLimitMb}MB", + CompilerOutput = compilerOutput + }; + } + + // Copy compiled executable back if successful + if (isolateResult.ExitCode == 0 && File.Exists(boxOutputPath)) + { + Directory.CreateDirectory(Path.GetDirectoryName(outputExecutablePath)!); + File.Copy(boxOutputPath, outputExecutablePath, overwrite: true); + + _logger.LogInformation("Compilation successful in Isolate box {BoxId}", boxId); + return new CompilationResult + { + Success = true, + ExecutablePath = outputExecutablePath, + CompilerOutput = compilerOutput + }; + } + + // Compilation failed + _logger.LogWarning("Compilation failed with exit code {ExitCode} in box {BoxId}", + isolateResult.ExitCode, boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation failed with exit code {isolateResult.ExitCode}", + CompilerOutput = compilerOutput + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Isolate compilation"); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation error: {ex.Message}" + }; + } + finally + { + // Cleanup box + if (boxId >= 0) + { + try + { + await _isolateService.CleanupBoxAsync(boxId); + _logger.LogDebug("Cleaned up isolate box {BoxId}", boxId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to cleanup compilation box {BoxId}", boxId); + } + + // Release box back to pool + _boxPool.ReleaseBox(boxId); + _logger.LogDebug("Released compilation box {BoxId} back to pool", boxId); + } + } + } + + private (string compiler, List compilerFlags) ResolveVersion(string? version) + { + var defaultCompiler = _configuration["Cpp:Compiler"] ?? "g++"; + var defaultFlags = SplitFlags(_configuration["Cpp:CompilerFlags"] ?? "-O2 -std=c++17 -Wall"); + + if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase)) + { + return (defaultCompiler, defaultFlags); + } + + var versionKey = $"Cpp:Versions:{version}"; + var versionCompiler = _configuration[$"{versionKey}:Compiler"]; + var versionFlagsValue = _configuration[$"{versionKey}:CompilerFlags"]; + + if (!string.IsNullOrEmpty(versionCompiler) || !string.IsNullOrEmpty(versionFlagsValue)) + { + var resolvedFlags = !string.IsNullOrEmpty(versionFlagsValue) + ? SplitFlags(versionFlagsValue) + : defaultFlags; + + _logger.LogInformation("Using C++ version {Version} configuration", version); + return (versionCompiler ?? defaultCompiler, resolvedFlags); + } + + var normalized = NormalizeCppVersion(version); + if (normalized != null) + { + var flagsWithoutStd = defaultFlags + .Where(flag => !flag.StartsWith("-std=", StringComparison.OrdinalIgnoreCase)) + .ToList(); + flagsWithoutStd.Add($"-std=c++{normalized}"); + _logger.LogInformation("Using inferred C++ standard c++{Standard}", normalized); + return (defaultCompiler, flagsWithoutStd); + } + + _logger.LogWarning("C++ version {Version} not found in configuration, using default", version); + return (defaultCompiler, defaultFlags); + } + + private static List SplitFlags(string flags) => + flags.Split(' ', StringSplitOptions.RemoveEmptyEntries) + .ToList(); + + private static string? NormalizeCppVersion(string version) + { + var cleaned = version.Trim().ToLowerInvariant(); + cleaned = cleaned.Replace("c++", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace("gnu++", string.Empty, StringComparison.OrdinalIgnoreCase) + .Trim('+', ' '); + + cleaned = cleaned switch + { + "2b" => "23", + "2a" => "20", + "1z" => "17", + "0x" => "11", + _ => cleaned + }; + + return cleaned switch + { + "26" => "26", + "23" => "23", + "20" => "20", + "17" => "17", + "14" => "14", + "11" => "11", + _ => null + }; + } +} diff --git a/src/LiquidCode.Tester.Worker/Services/CppExecutionServiceIsolate.cs b/src/LiquidCode.Tester.Worker/Services/CppExecutionServiceIsolate.cs new file mode 100644 index 0000000..1c3789b --- /dev/null +++ b/src/LiquidCode.Tester.Worker/Services/CppExecutionServiceIsolate.cs @@ -0,0 +1,179 @@ +using System.Diagnostics; +using LiquidCode.Tester.Worker.Services.Isolate; + +namespace LiquidCode.Tester.Worker.Services; + +/// +/// C++ program execution service using Isolate sandbox +/// +public class CppExecutionServiceIsolate : IExecutionService +{ + private readonly ILogger _logger; + private readonly IsolateService _isolateService; + private readonly IsolateBoxPool _boxPool; + + public CppExecutionServiceIsolate( + ILogger logger, + IsolateService isolateService, + IsolateBoxPool boxPool) + { + _logger = logger; + _isolateService = isolateService; + _boxPool = boxPool; + } + + public async Task ExecuteAsync( + string executablePath, + string inputFilePath, + int timeLimitMs, + int memoryLimitMb) + { + _logger.LogInformation( + "Executing {Executable} with Isolate: time={TimeLimit}ms, memory={MemoryLimit}MB", + executablePath, timeLimitMs, memoryLimitMb); + + var result = new ExecutionResult(); + var stopwatch = Stopwatch.StartNew(); + int boxId = -1; + + try + { + // Acquire a box from pool + boxId = await _boxPool.AcquireBoxAsync(); + _logger.LogDebug("Acquired isolate box {BoxId}", boxId); + + // Initialize the box + await _isolateService.InitBoxAsync(boxId); + + // Copy executable to box + var boxDir = $"/var/local/lib/isolate/{boxId}/box"; + var executableName = Path.GetFileName(executablePath); + var boxExecutablePath = Path.Combine(boxDir, executableName); + + File.Copy(executablePath, boxExecutablePath, overwrite: true); + + // Make executable + var chmodProcess = Process.Start(new ProcessStartInfo + { + FileName = "chmod", + Arguments = $"+x {boxExecutablePath}", + UseShellExecute = false + }); + chmodProcess?.WaitForExit(); + + // Prepare output file in box + var outputFilePath = Path.Combine(boxDir, "output.txt"); + + // Run in Isolate + var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions + { + BoxId = boxId, + Executable = $"/box/{executableName}", + TimeLimitSeconds = timeLimitMs / 1000.0, + WallTimeLimitSeconds = (timeLimitMs / 1000.0) * 2, + MemoryLimitKb = memoryLimitMb * 1024, + StackLimitKb = 256 * 1024, // 256 MB stack + ProcessLimit = 1, // Single process only + EnableNetwork = false, // No network access + StdinFile = inputFilePath, + StdoutFile = outputFilePath, + WorkingDirectory = "/box" + }); + + stopwatch.Stop(); + + // Read output + if (File.Exists(outputFilePath)) + { + result.Output = await File.ReadAllTextAsync(outputFilePath); + } + + // Map Isolate result to ExecutionResult + result.ExecutionTimeMs = (long)(isolateResult.CpuTimeSeconds * 1000); + result.MemoryUsedMb = isolateResult.MemoryUsedKb / 1024; + result.ErrorOutput = isolateResult.ErrorOutput; + result.ExitCode = isolateResult.ExitCode; + + if (isolateResult.TimeLimitExceeded) + { + result.TimeLimitExceeded = true; + result.ErrorMessage = $"Time limit exceeded: {isolateResult.CpuTimeSeconds:F3}s"; + _logger.LogWarning("Time limit exceeded for box {BoxId}", boxId); + } + else if (isolateResult.MemoryLimitExceeded) + { + result.MemoryLimitExceeded = true; + result.ErrorMessage = $"Memory limit exceeded: {isolateResult.MemoryUsedKb / 1024}MB"; + if (isolateResult.CgroupOomKilled) + { + result.ErrorMessage += " (OOM killed by cgroup)"; + } + _logger.LogWarning("Memory limit exceeded for box {BoxId}", boxId); + } + else if (isolateResult.RuntimeError) + { + result.RuntimeError = true; + result.ErrorMessage = $"Runtime error: {isolateResult.Message}"; + if (isolateResult.ExitSignal.HasValue) + { + result.ErrorMessage += $" (signal {isolateResult.ExitSignal})"; + } + _logger.LogWarning("Runtime error for box {BoxId}: {Message}", boxId, isolateResult.Message); + } + else if (isolateResult.ExitCode == 0) + { + result.Success = true; + _logger.LogInformation( + "Execution successful: time={Time}ms, memory={Memory}MB, box={BoxId}", + result.ExecutionTimeMs, result.MemoryUsedMb, boxId); + } + else + { + result.RuntimeError = true; + result.ErrorMessage = $"Non-zero exit code: {isolateResult.ExitCode}"; + _logger.LogWarning("Non-zero exit code {ExitCode} for box {BoxId}", + isolateResult.ExitCode, boxId); + } + + // Log detailed statistics + _logger.LogDebug( + "Isolate stats: CPU={Cpu}s, Wall={Wall}s, Memory={Mem}KB, " + + "VoluntaryContextSwitches={Vol}, ForcedContextSwitches={Forced}", + isolateResult.CpuTimeSeconds, + isolateResult.WallTimeSeconds, + isolateResult.MemoryUsedKb, + isolateResult.VoluntaryContextSwitches, + isolateResult.ForcedContextSwitches); + } + catch (Exception ex) + { + stopwatch.Stop(); + result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds; + result.RuntimeError = true; + result.ErrorMessage = $"Execution error: {ex.Message}"; + _logger.LogError(ex, "Error during Isolate execution"); + } + finally + { + // Cleanup box + if (boxId >= 0) + { + try + { + await _isolateService.CleanupBoxAsync(boxId); + _logger.LogDebug("Cleaned up isolate box {BoxId}", boxId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to cleanup box {BoxId}", boxId); + } + + // Release box back to pool + _boxPool.ReleaseBox(boxId); + _logger.LogDebug("Released box {BoxId} back to pool", boxId); + } + } + + return result; + } +} diff --git a/src/LiquidCode.Tester.Worker/Services/ExecutionServiceFactory.cs b/src/LiquidCode.Tester.Worker/Services/ExecutionServiceFactory.cs index eda5933..3c32ac3 100644 --- a/src/LiquidCode.Tester.Worker/Services/ExecutionServiceFactory.cs +++ b/src/LiquidCode.Tester.Worker/Services/ExecutionServiceFactory.cs @@ -1,29 +1,36 @@ namespace LiquidCode.Tester.Worker.Services; +/// +/// Factory for execution services - always uses Isolate sandbox for security +/// public class ExecutionServiceFactory : IExecutionServiceFactory { private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; - public ExecutionServiceFactory(IServiceProvider serviceProvider, ILogger logger) + public ExecutionServiceFactory( + IServiceProvider serviceProvider, + ILogger logger) { _serviceProvider = serviceProvider; _logger = logger; + + _logger.LogInformation("Execution services configured to use Isolate sandbox"); } public IExecutionService GetExecutionService(string language) { var normalizedLanguage = language.ToLowerInvariant().Replace(" ", ""); - _logger.LogInformation("Getting execution service for language: {Language}", normalizedLanguage); + _logger.LogDebug("Getting Isolate execution service for language: {Language}", normalizedLanguage); return normalizedLanguage switch { - "c++" or "cpp" => _serviceProvider.GetRequiredService(), - "java" => _serviceProvider.GetRequiredService(), - "kotlin" => _serviceProvider.GetRequiredService(), - "c#" or "csharp" => _serviceProvider.GetRequiredService(), - "python" => _serviceProvider.GetRequiredService(), + "c++" or "cpp" => _serviceProvider.GetRequiredService(), + "java" => _serviceProvider.GetRequiredService(), + "kotlin" => _serviceProvider.GetRequiredService(), + "c#" or "csharp" => _serviceProvider.GetRequiredService(), + "python" => _serviceProvider.GetRequiredService(), _ => throw new NotSupportedException($"Language '{language}' is not supported") }; } diff --git a/src/LiquidCode.Tester.Worker/Services/Isolate/IsolateBoxPool.cs b/src/LiquidCode.Tester.Worker/Services/Isolate/IsolateBoxPool.cs new file mode 100644 index 0000000..dcc8055 --- /dev/null +++ b/src/LiquidCode.Tester.Worker/Services/Isolate/IsolateBoxPool.cs @@ -0,0 +1,80 @@ +using System.Collections.Concurrent; + +namespace LiquidCode.Tester.Worker.Services.Isolate; + +/// +/// Pool manager for Isolate sandbox boxes +/// Manages box IDs for parallel execution +/// +public class IsolateBoxPool +{ + private readonly ConcurrentBag _availableBoxes; + private readonly SemaphoreSlim _semaphore; + private readonly int _maxBoxes; + private readonly ILogger _logger; + + public IsolateBoxPool(int maxBoxes, ILogger logger) + { + if (maxBoxes <= 0) + throw new ArgumentException("Max boxes must be greater than 0", nameof(maxBoxes)); + + _maxBoxes = maxBoxes; + _logger = logger; + _availableBoxes = new ConcurrentBag(); + _semaphore = new SemaphoreSlim(maxBoxes, maxBoxes); + + // Initialize pool with box IDs + for (int i = 0; i < maxBoxes; i++) + { + _availableBoxes.Add(i); + } + + _logger.LogInformation("Initialized Isolate box pool with {MaxBoxes} boxes", maxBoxes); + } + + /// + /// Acquire a box ID from the pool (async, waits if all boxes are in use) + /// + public async Task AcquireBoxAsync(CancellationToken cancellationToken = default) + { + await _semaphore.WaitAsync(cancellationToken); + + if (_availableBoxes.TryTake(out var boxId)) + { + _logger.LogDebug("Acquired box {BoxId}, remaining: {Remaining}", + boxId, _availableBoxes.Count); + return boxId; + } + + // Should not happen due to semaphore, but handle anyway + _semaphore.Release(); + throw new InvalidOperationException("Failed to acquire box from pool"); + } + + /// + /// Release a box ID back to the pool + /// + public void ReleaseBox(int boxId) + { + if (boxId < 0 || boxId >= _maxBoxes) + { + _logger.LogWarning("Attempted to release invalid box ID: {BoxId}", boxId); + return; + } + + _availableBoxes.Add(boxId); + _semaphore.Release(); + + _logger.LogDebug("Released box {BoxId}, available: {Available}", + boxId, _availableBoxes.Count); + } + + /// + /// Get current pool statistics + /// + public (int Total, int Available, int InUse) GetStatistics() + { + var available = _availableBoxes.Count; + return (_maxBoxes, available, _maxBoxes - available); + } +} diff --git a/src/LiquidCode.Tester.Worker/Services/Isolate/IsolateExecutionResult.cs b/src/LiquidCode.Tester.Worker/Services/Isolate/IsolateExecutionResult.cs new file mode 100644 index 0000000..961242c --- /dev/null +++ b/src/LiquidCode.Tester.Worker/Services/Isolate/IsolateExecutionResult.cs @@ -0,0 +1,97 @@ +namespace LiquidCode.Tester.Worker.Services.Isolate; + +/// +/// Result of program execution in Isolate sandbox +/// +public class IsolateExecutionResult +{ + /// + /// Standard output from the program + /// + public string Output { get; set; } = string.Empty; + + /// + /// Standard error output from the program + /// + public string ErrorOutput { get; set; } = string.Empty; + + /// + /// CPU time used in seconds + /// + public double CpuTimeSeconds { get; set; } + + /// + /// Wall (real) time used in seconds + /// + public double WallTimeSeconds { get; set; } + + /// + /// Memory used in kilobytes (from cgroups) + /// + public long MemoryUsedKb { get; set; } + + /// + /// Maximum resident set size in kilobytes + /// + public long MaxRssKb { get; set; } + + /// + /// Exit code of the program + /// + public int ExitCode { get; set; } + + /// + /// Exit signal if program was killed by signal + /// + public int? ExitSignal { get; set; } + + /// + /// Status code from isolate (RE/SG/TO/XX/OK) + /// + public string Status { get; set; } = string.Empty; + + /// + /// Additional message from isolate + /// + public string Message { get; set; } = string.Empty; + + /// + /// Whether the program was killed + /// + public bool WasKilled { get; set; } + + /// + /// Whether time limit was exceeded + /// + public bool TimeLimitExceeded { get; set; } + + /// + /// Whether memory limit was exceeded + /// + public bool MemoryLimitExceeded { get; set; } + + /// + /// Whether program had runtime error + /// + public bool RuntimeError { get; set; } + + /// + /// Whether process was killed by cgroup OOM killer + /// + public bool CgroupOomKilled { get; set; } + + /// + /// Number of voluntary context switches + /// + public int VoluntaryContextSwitches { get; set; } + + /// + /// Number of forced context switches + /// + public int ForcedContextSwitches { get; set; } + + /// + /// Whether execution was successful (no errors, no limits exceeded) + /// + public bool Success => ExitCode == 0 && !TimeLimitExceeded && !MemoryLimitExceeded && !RuntimeError; +} diff --git a/src/LiquidCode.Tester.Worker/Services/Isolate/IsolateRunOptions.cs b/src/LiquidCode.Tester.Worker/Services/Isolate/IsolateRunOptions.cs new file mode 100644 index 0000000..9374c8b --- /dev/null +++ b/src/LiquidCode.Tester.Worker/Services/Isolate/IsolateRunOptions.cs @@ -0,0 +1,103 @@ +namespace LiquidCode.Tester.Worker.Services.Isolate; + +/// +/// Options for running a program in Isolate sandbox +/// +public class IsolateRunOptions +{ + /// + /// Box ID for parallel execution (0-999) + /// + public int BoxId { get; set; } + + /// + /// Path to executable to run + /// + public string Executable { get; set; } = string.Empty; + + /// + /// Command-line arguments for the executable + /// + public string[]? Arguments { get; set; } + + /// + /// CPU time limit in seconds (0 = no limit) + /// + public double TimeLimitSeconds { get; set; } + + /// + /// Wall time limit in seconds (0 = no limit) + /// + public double WallTimeLimitSeconds { get; set; } + + /// + /// Memory limit in kilobytes (0 = no limit) + /// + public long MemoryLimitKb { get; set; } + + /// + /// Stack size limit in kilobytes (0 = default) + /// + public long StackLimitKb { get; set; } + + /// + /// Maximum number of processes (0 = no limit, 1 = single process) + /// + public int ProcessLimit { get; set; } = 1; + + /// + /// Enable network access (default: false) + /// + public bool EnableNetwork { get; set; } = false; + + /// + /// Path to file for stdin redirection + /// + public string? StdinFile { get; set; } + + /// + /// Path to file for stdout redirection + /// + public string? StdoutFile { get; set; } + + /// + /// Path to file for stderr redirection + /// + public string? StderrFile { get; set; } + + /// + /// Working directory inside sandbox + /// + public string? WorkingDirectory { get; set; } + + /// + /// Directory bindings (host path -> sandbox path) + /// + public List? DirectoryBindings { get; set; } + + /// + /// Environment variables to set + /// + public Dictionary? EnvironmentVariables { get; set; } +} + +/// +/// Directory binding for isolate sandbox +/// +public class DirectoryBinding +{ + /// + /// Path on host system + /// + public string HostPath { get; set; } = string.Empty; + + /// + /// Path inside sandbox + /// + public string SandboxPath { get; set; } = string.Empty; + + /// + /// Read-only binding (default: true) + /// + public bool ReadOnly { get; set; } = true; +} diff --git a/src/LiquidCode.Tester.Worker/Services/Isolate/IsolateService.cs b/src/LiquidCode.Tester.Worker/Services/Isolate/IsolateService.cs new file mode 100644 index 0000000..8f6b65f --- /dev/null +++ b/src/LiquidCode.Tester.Worker/Services/Isolate/IsolateService.cs @@ -0,0 +1,349 @@ +using System.Diagnostics; +using System.Text; + +namespace LiquidCode.Tester.Worker.Services.Isolate; + +/// +/// Service for running programs in Isolate sandbox +/// +public class IsolateService +{ + private readonly ILogger _logger; + private readonly string _isolatePath; + + public IsolateService(ILogger logger) + { + _logger = logger; + _isolatePath = FindIsolatePath(); + } + + /// + /// Initialize a sandbox box + /// + public async Task InitBoxAsync(int boxId) + { + _logger.LogDebug("Initializing isolate box {BoxId}", boxId); + + var result = await RunIsolateCommandAsync($"--box-id={boxId} --init"); + + if (result.ExitCode != 0) + { + throw new InvalidOperationException( + $"Failed to initialize isolate box {boxId}: {result.Error}"); + } + + _logger.LogDebug("Box {BoxId} initialized at {Path}", boxId, result.Output.Trim()); + } + + /// + /// Execute program in sandbox + /// + public async Task RunAsync(IsolateRunOptions options) + { + _logger.LogInformation("Running in isolate box {BoxId}: {Executable}", + options.BoxId, options.Executable); + + // Create metadata file path + var metaFile = Path.Combine(Path.GetTempPath(), $"isolate_meta_{options.BoxId}_{Guid.NewGuid()}.txt"); + + try + { + // Build isolate command + var command = BuildRunCommand(options, metaFile); + + _logger.LogDebug("Isolate command: {Command}", command); + + // Execute + var cmdResult = await RunIsolateCommandAsync(command); + + // Parse metadata + var result = await ParseMetadataAsync(metaFile); + result.Output = cmdResult.Output; + result.ErrorOutput = cmdResult.Error; + + _logger.LogInformation("Execution completed: time={Time}s, memory={Memory}KB, exitcode={ExitCode}", + result.CpuTimeSeconds, result.MemoryUsedKb, result.ExitCode); + + return result; + } + finally + { + // Cleanup metadata file + try + { + if (File.Exists(metaFile)) + { + File.Delete(metaFile); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to delete metadata file {MetaFile}", metaFile); + } + } + } + + /// + /// Cleanup sandbox box + /// + public async Task CleanupBoxAsync(int boxId) + { + _logger.LogDebug("Cleaning up isolate box {BoxId}", boxId); + + var result = await RunIsolateCommandAsync($"--box-id={boxId} --cleanup"); + + if (result.ExitCode != 0) + { + _logger.LogWarning("Failed to cleanup isolate box {BoxId}: {Error}", boxId, result.Error); + } + } + + /// + /// Build isolate run command from options + /// + private string BuildRunCommand(IsolateRunOptions options, string metaFile) + { + var args = new List + { + $"--box-id={options.BoxId}", + $"--meta={metaFile}", + "--cg", // Enable cgroups + "--silent" // Suppress status messages + }; + + // Time limits + if (options.TimeLimitSeconds > 0) + { + args.Add($"--time={options.TimeLimitSeconds:F3}"); + } + + if (options.WallTimeLimitSeconds > 0) + { + args.Add($"--wall-time={options.WallTimeLimitSeconds:F3}"); + } + + // Memory limit + if (options.MemoryLimitKb > 0) + { + args.Add($"--cg-mem={options.MemoryLimitKb}"); + } + + // Process limit + if (options.ProcessLimit > 0) + { + args.Add($"--processes={options.ProcessLimit}"); + } + else + { + args.Add("--processes=1"); // Default: single process + } + + // Stack size + if (options.StackLimitKb > 0) + { + args.Add($"--stack={options.StackLimitKb}"); + } + + // Network isolation (default: disabled) + if (!options.EnableNetwork) + { + // Network is isolated by default in isolate + } + else + { + args.Add("--share-net"); + } + + // I/O redirection + if (!string.IsNullOrEmpty(options.StdinFile)) + { + args.Add($"--stdin={options.StdinFile}"); + } + + if (!string.IsNullOrEmpty(options.StdoutFile)) + { + args.Add($"--stdout={options.StdoutFile}"); + } + + if (!string.IsNullOrEmpty(options.StderrFile)) + { + args.Add($"--stderr={options.StderrFile}"); + } + + // Working directory + if (!string.IsNullOrEmpty(options.WorkingDirectory)) + { + args.Add($"--chdir={options.WorkingDirectory}"); + } + + // Directory bindings + if (options.DirectoryBindings != null) + { + foreach (var binding in options.DirectoryBindings) + { + var dirSpec = binding.ReadOnly + ? $"--dir={binding.HostPath}={binding.SandboxPath}:ro" + : $"--dir={binding.HostPath}={binding.SandboxPath}:rw"; + args.Add(dirSpec); + } + } + + // Environment variables + if (options.EnvironmentVariables != null) + { + foreach (var env in options.EnvironmentVariables) + { + args.Add($"--env={env.Key}={env.Value}"); + } + } + + // Run command + args.Add("--run"); + args.Add("--"); + args.Add(options.Executable); + + if (options.Arguments != null) + { + args.AddRange(options.Arguments); + } + + return string.Join(" ", args); + } + + /// + /// Parse isolate metadata file + /// + private async Task ParseMetadataAsync(string metaFile) + { + var result = new IsolateExecutionResult(); + + if (!File.Exists(metaFile)) + { + _logger.LogWarning("Metadata file not found: {MetaFile}", metaFile); + return result; + } + + var lines = await File.ReadAllLinesAsync(metaFile); + + foreach (var line in lines) + { + var parts = line.Split(':', 2); + if (parts.Length != 2) continue; + + var key = parts[0].Trim(); + var value = parts[1].Trim(); + + switch (key) + { + case "time": + if (double.TryParse(value, out var time)) + result.CpuTimeSeconds = time; + break; + + case "time-wall": + if (double.TryParse(value, out var wallTime)) + result.WallTimeSeconds = wallTime; + break; + + case "max-rss": + if (long.TryParse(value, out var rss)) + result.MaxRssKb = rss; + break; + + case "cg-mem": + if (long.TryParse(value, out var cgMem)) + result.MemoryUsedKb = cgMem; + break; + + case "exitcode": + if (int.TryParse(value, out var exitCode)) + result.ExitCode = exitCode; + break; + + case "exitsig": + if (int.TryParse(value, out var exitSig)) + result.ExitSignal = exitSig; + break; + + case "status": + result.Status = value; + result.TimeLimitExceeded = value == "TO"; + result.MemoryLimitExceeded = value == "XX" || value == "MLE"; + result.RuntimeError = value == "RE" || value == "SG"; + break; + + case "message": + result.Message = value; + break; + + case "killed": + result.WasKilled = value == "1"; + break; + + case "cg-oom-killed": + result.CgroupOomKilled = value == "1"; + result.MemoryLimitExceeded = true; + break; + + case "csw-voluntary": + if (int.TryParse(value, out var csvVoluntary)) + result.VoluntaryContextSwitches = csvVoluntary; + break; + + case "csw-forced": + if (int.TryParse(value, out var csvForced)) + result.ForcedContextSwitches = csvForced; + break; + } + } + + return result; + } + + /// + /// Execute isolate command + /// + private async Task<(int ExitCode, string Output, string Error)> RunIsolateCommandAsync(string arguments) + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = _isolatePath, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + process.Start(); + + var outputTask = process.StandardOutput.ReadToEndAsync(); + var errorTask = process.StandardError.ReadToEndAsync(); + + await process.WaitForExitAsync(); + + return (process.ExitCode, await outputTask, await errorTask); + } + + /// + /// Find isolate binary path + /// + private string FindIsolatePath() + { + var paths = new[] { "/usr/local/bin/isolate", "/usr/bin/isolate" }; + + foreach (var path in paths) + { + if (File.Exists(path)) + { + _logger.LogInformation("Found isolate at {Path}", path); + return path; + } + } + + throw new FileNotFoundException("Isolate binary not found. Make sure isolate is installed."); + } +} diff --git a/src/LiquidCode.Tester.Worker/Services/JavaCompilationServiceIsolate.cs b/src/LiquidCode.Tester.Worker/Services/JavaCompilationServiceIsolate.cs new file mode 100644 index 0000000..09be33c --- /dev/null +++ b/src/LiquidCode.Tester.Worker/Services/JavaCompilationServiceIsolate.cs @@ -0,0 +1,219 @@ +using LiquidCode.Tester.Worker.Services.Isolate; +using LiquidCode.Tester.Worker.Models; + +namespace LiquidCode.Tester.Worker.Services; + +/// +/// Java compilation service using Isolate sandbox for security +/// +public class JavaCompilationServiceIsolate : ICompilationService +{ + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + private readonly IsolateService _isolateService; + private readonly IsolateBoxPool _boxPool; + + private const int CompilationTimeLimitSeconds = 30; + private const int CompilationMemoryLimitMb = 512; + + public JavaCompilationServiceIsolate( + ILogger logger, + IConfiguration configuration, + IsolateService isolateService, + IsolateBoxPool boxPool) + { + _logger = logger; + _configuration = configuration; + _isolateService = isolateService; + _boxPool = boxPool; + } + + public async Task 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 with Isolate in {WorkingDirectory} with version {Version}", + workingDirectory, version ?? "latest"); + + try + { + await File.WriteAllTextAsync(sourceFilePath, sourceCode); + return await CompileFileInIsolateAsync(sourceFilePath, classFilePath, workingDirectory, version); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Isolate Java compilation"); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation error: {ex.Message}" + }; + } + } + + private async Task CompileFileInIsolateAsync( + string sourceFilePath, + string classFilePath, + string workingDirectory, + string? version = null) + { + int boxId = -1; + + try + { + boxId = await _boxPool.AcquireBoxAsync(); + _logger.LogDebug("Acquired isolate box {BoxId} for Java compilation", boxId); + + await _isolateService.InitBoxAsync(boxId); + + var boxDir = $"/var/local/lib/isolate/{boxId}/box"; + var sourceFileName = Path.GetFileName(sourceFilePath); + var boxSourcePath = Path.Combine(boxDir, sourceFileName); + + File.Copy(sourceFilePath, boxSourcePath, overwrite: true); + + var (compiler, compilerFlags) = ResolveVersion(version); + _logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, compilerFlags); + + // Build arguments + var arguments = new List(); + if (!string.IsNullOrWhiteSpace(compilerFlags)) + { + arguments.AddRange(compilerFlags.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + } + arguments.Add($"/box/{sourceFileName}"); + + var stderrFilePath = Path.Combine(boxDir, "compile_stderr.txt"); + + var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions + { + BoxId = boxId, + Executable = $"/usr/bin/{compiler}", + Arguments = arguments.ToArray(), + TimeLimitSeconds = CompilationTimeLimitSeconds, + WallTimeLimitSeconds = CompilationTimeLimitSeconds * 2, + MemoryLimitKb = CompilationMemoryLimitMb * 1024, + StackLimitKb = 256 * 1024, + ProcessLimit = 10, // javac may spawn multiple processes + EnableNetwork = false, + StderrFile = stderrFilePath, + WorkingDirectory = "/box", + DirectoryBindings = new List + { + new DirectoryBinding { HostPath = "/usr/lib", SandboxPath = "/usr/lib", ReadOnly = true }, + new DirectoryBinding { HostPath = "/lib", SandboxPath = "/lib", ReadOnly = true } + } + }); + + var compilerOutput = string.Empty; + if (File.Exists(stderrFilePath)) + { + compilerOutput = await File.ReadAllTextAsync(stderrFilePath); + } + if (!string.IsNullOrEmpty(isolateResult.ErrorOutput)) + { + compilerOutput = $"{compilerOutput}\n{isolateResult.ErrorOutput}".Trim(); + } + + if (isolateResult.TimeLimitExceeded) + { + _logger.LogWarning("Java compilation time limit exceeded for box {BoxId}", boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation timeout: exceeded {CompilationTimeLimitSeconds}s limit", + CompilerOutput = compilerOutput + }; + } + + if (isolateResult.MemoryLimitExceeded) + { + _logger.LogWarning("Java compilation memory limit exceeded for box {BoxId}", boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation memory limit exceeded: {CompilationMemoryLimitMb}MB", + CompilerOutput = compilerOutput + }; + } + + // Copy .class file back + var boxClassPath = Path.Combine(boxDir, "Solution.class"); + if (isolateResult.ExitCode == 0 && File.Exists(boxClassPath)) + { + Directory.CreateDirectory(Path.GetDirectoryName(classFilePath)!); + File.Copy(boxClassPath, classFilePath, overwrite: true); + + _logger.LogInformation("Java compilation successful in Isolate box {BoxId}", boxId); + return new CompilationResult + { + Success = true, + ExecutablePath = classFilePath, + CompilerOutput = compilerOutput + }; + } + + _logger.LogWarning("Java compilation failed with exit code {ExitCode} in box {BoxId}", + isolateResult.ExitCode, boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation failed with exit code {isolateResult.ExitCode}", + CompilerOutput = compilerOutput + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Isolate Java compilation"); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation error: {ex.Message}" + }; + } + finally + { + if (boxId >= 0) + { + try + { + await _isolateService.CleanupBoxAsync(boxId); + _logger.LogDebug("Cleaned up isolate box {BoxId}", boxId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to cleanup compilation box {BoxId}", boxId); + } + + _boxPool.ReleaseBox(boxId); + _logger.LogDebug("Released compilation box {BoxId} back to pool", boxId); + } + } + } + + private (string compiler, string compilerFlags) ResolveVersion(string? version) + { + if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase)) + { + var compiler = _configuration["Java:Compiler"] ?? "javac"; + var compilerFlags = _configuration["Java:CompilerFlags"] ?? ""; + return (compiler, compilerFlags); + } + + 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 ?? ""); + } + + _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); + } +} diff --git a/src/LiquidCode.Tester.Worker/Services/JavaExecutionServiceIsolate.cs b/src/LiquidCode.Tester.Worker/Services/JavaExecutionServiceIsolate.cs new file mode 100644 index 0000000..27fc0b9 --- /dev/null +++ b/src/LiquidCode.Tester.Worker/Services/JavaExecutionServiceIsolate.cs @@ -0,0 +1,175 @@ +using System.Diagnostics; +using LiquidCode.Tester.Worker.Services.Isolate; + +namespace LiquidCode.Tester.Worker.Services; + +/// +/// Java program execution service using Isolate sandbox +/// +public class JavaExecutionServiceIsolate : IExecutionService +{ + private readonly ILogger _logger; + private readonly IsolateService _isolateService; + private readonly IsolateBoxPool _boxPool; + + public JavaExecutionServiceIsolate( + ILogger logger, + IsolateService isolateService, + IsolateBoxPool boxPool) + { + _logger = logger; + _isolateService = isolateService; + _boxPool = boxPool; + } + + public async Task ExecuteAsync( + string executablePath, + string inputFilePath, + int timeLimitMs, + int memoryLimitMb) + { + var workingDirectory = Path.GetDirectoryName(executablePath)!; + _logger.LogInformation( + "Executing Java with Isolate from {WorkingDirectory}: time={TimeLimit}ms, memory={MemoryLimit}MB", + workingDirectory, timeLimitMs, memoryLimitMb); + + var result = new ExecutionResult(); + var stopwatch = Stopwatch.StartNew(); + int boxId = -1; + + try + { + // Acquire a box from pool + boxId = await _boxPool.AcquireBoxAsync(); + _logger.LogDebug("Acquired isolate box {BoxId}", boxId); + + // Initialize the box + await _isolateService.InitBoxAsync(boxId); + + // Copy all .class files to box (Java needs classpath) + var boxDir = $"/var/local/lib/isolate/{boxId}/box"; + + // Copy all files from working directory (includes Solution.class and dependencies) + foreach (var file in Directory.GetFiles(workingDirectory, "*.*")) + { + var fileName = Path.GetFileName(file); + var destPath = Path.Combine(boxDir, fileName); + File.Copy(file, destPath, overwrite: true); + } + + // Prepare output file in box + var outputFilePath = Path.Combine(boxDir, "output.txt"); + + // Run in Isolate + // Note: Java needs more memory for JVM overhead + var javaMemoryMb = Math.Max(memoryLimitMb, 128); // Minimum 128MB for JVM + + var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions + { + BoxId = boxId, + Executable = "/usr/bin/java", + Arguments = new[] { "-cp", "/box", "Solution" }, + TimeLimitSeconds = timeLimitMs / 1000.0, + WallTimeLimitSeconds = (timeLimitMs / 1000.0) * 2, + MemoryLimitKb = javaMemoryMb * 1024, + StackLimitKb = 256 * 1024, // 256 MB stack + ProcessLimit = 64, // Java creates multiple threads + EnableNetwork = false, + StdinFile = inputFilePath, + StdoutFile = outputFilePath, + WorkingDirectory = "/box" + }); + + stopwatch.Stop(); + + // Read output + if (File.Exists(outputFilePath)) + { + result.Output = await File.ReadAllTextAsync(outputFilePath); + } + + // Map Isolate result to ExecutionResult + result.ExecutionTimeMs = (long)(isolateResult.CpuTimeSeconds * 1000); + result.MemoryUsedMb = isolateResult.MemoryUsedKb / 1024; + result.ErrorOutput = isolateResult.ErrorOutput; + result.ExitCode = isolateResult.ExitCode; + + if (isolateResult.TimeLimitExceeded) + { + result.TimeLimitExceeded = true; + result.ErrorMessage = $"Time limit exceeded: {isolateResult.CpuTimeSeconds:F3}s"; + _logger.LogWarning("Time limit exceeded for box {BoxId}", boxId); + } + else if (isolateResult.MemoryLimitExceeded) + { + result.MemoryLimitExceeded = true; + result.ErrorMessage = $"Memory limit exceeded: {isolateResult.MemoryUsedKb / 1024}MB"; + if (isolateResult.CgroupOomKilled) + { + result.ErrorMessage += " (OOM killed by cgroup)"; + } + _logger.LogWarning("Memory limit exceeded for box {BoxId}", boxId); + } + else if (isolateResult.RuntimeError) + { + result.RuntimeError = true; + result.ErrorMessage = $"Runtime error: {isolateResult.Message}"; + if (isolateResult.ExitSignal.HasValue) + { + result.ErrorMessage += $" (signal {isolateResult.ExitSignal})"; + } + _logger.LogWarning("Runtime error for box {BoxId}: {Message}", boxId, isolateResult.Message); + } + else if (isolateResult.ExitCode == 0) + { + result.Success = true; + _logger.LogInformation( + "Execution successful: time={Time}ms, memory={Memory}MB, box={BoxId}", + result.ExecutionTimeMs, result.MemoryUsedMb, boxId); + } + else + { + result.RuntimeError = true; + result.ErrorMessage = $"Non-zero exit code: {isolateResult.ExitCode}"; + _logger.LogWarning("Non-zero exit code {ExitCode} for box {BoxId}", + isolateResult.ExitCode, boxId); + } + + _logger.LogDebug( + "Isolate stats: CPU={Cpu}s, Wall={Wall}s, Memory={Mem}KB", + isolateResult.CpuTimeSeconds, + isolateResult.WallTimeSeconds, + isolateResult.MemoryUsedKb); + } + catch (Exception ex) + { + stopwatch.Stop(); + result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds; + result.RuntimeError = true; + result.ErrorMessage = $"Execution error: {ex.Message}"; + _logger.LogError(ex, "Error during Isolate execution"); + } + finally + { + // Cleanup box + if (boxId >= 0) + { + try + { + await _isolateService.CleanupBoxAsync(boxId); + _logger.LogDebug("Cleaned up isolate box {BoxId}", boxId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to cleanup box {BoxId}", boxId); + } + + // Release box back to pool + _boxPool.ReleaseBox(boxId); + _logger.LogDebug("Released box {BoxId} back to pool", boxId); + } + } + + return result; + } +} diff --git a/src/LiquidCode.Tester.Worker/Services/KotlinCompilationServiceIsolate.cs b/src/LiquidCode.Tester.Worker/Services/KotlinCompilationServiceIsolate.cs new file mode 100644 index 0000000..6eaf5ca --- /dev/null +++ b/src/LiquidCode.Tester.Worker/Services/KotlinCompilationServiceIsolate.cs @@ -0,0 +1,221 @@ +using LiquidCode.Tester.Worker.Services.Isolate; +using LiquidCode.Tester.Worker.Models; + +namespace LiquidCode.Tester.Worker.Services; + +/// +/// Kotlin compilation service using Isolate sandbox for security +/// +public class KotlinCompilationServiceIsolate : ICompilationService +{ + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + private readonly IsolateService _isolateService; + private readonly IsolateBoxPool _boxPool; + + private const int CompilationTimeLimitSeconds = 30; + private const int CompilationMemoryLimitMb = 512; + + public KotlinCompilationServiceIsolate( + ILogger logger, + IConfiguration configuration, + IsolateService isolateService, + IsolateBoxPool boxPool) + { + _logger = logger; + _configuration = configuration; + _isolateService = isolateService; + _boxPool = boxPool; + } + + public async Task 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 with Isolate in {WorkingDirectory} with version {Version}", + workingDirectory, version ?? "latest"); + + try + { + await File.WriteAllTextAsync(sourceFilePath, sourceCode); + return await CompileFileInIsolateAsync(sourceFilePath, jarFilePath, version); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Isolate Kotlin compilation"); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation error: {ex.Message}" + }; + } + } + + private async Task CompileFileInIsolateAsync( + string sourceFilePath, + string jarFilePath, + string? version = null) + { + int boxId = -1; + + try + { + boxId = await _boxPool.AcquireBoxAsync(); + _logger.LogDebug("Acquired isolate box {BoxId} for Kotlin compilation", boxId); + + await _isolateService.InitBoxAsync(boxId); + + var boxDir = $"/var/local/lib/isolate/{boxId}/box"; + var sourceFileName = Path.GetFileName(sourceFilePath); + var boxSourcePath = Path.Combine(boxDir, sourceFileName); + var jarFileName = "solution.jar"; + var boxJarPath = Path.Combine(boxDir, jarFileName); + + File.Copy(sourceFilePath, boxSourcePath, overwrite: true); + + var (compiler, compilerFlags) = ResolveVersion(version); + _logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, compilerFlags); + + var arguments = new List(); + if (!string.IsNullOrWhiteSpace(compilerFlags)) + { + arguments.AddRange(compilerFlags.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + } + arguments.Add($"/box/{sourceFileName}"); + arguments.Add("-include-runtime"); + arguments.Add("-d"); + arguments.Add($"/box/{jarFileName}"); + + var stderrFilePath = Path.Combine(boxDir, "compile_stderr.txt"); + + var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions + { + BoxId = boxId, + Executable = $"/usr/local/bin/{compiler}", + Arguments = arguments.ToArray(), + TimeLimitSeconds = CompilationTimeLimitSeconds, + WallTimeLimitSeconds = CompilationTimeLimitSeconds * 2, + MemoryLimitKb = CompilationMemoryLimitMb * 1024, + StackLimitKb = 256 * 1024, + ProcessLimit = 10, + EnableNetwork = false, + StderrFile = stderrFilePath, + WorkingDirectory = "/box", + DirectoryBindings = new List + { + new DirectoryBinding { HostPath = "/usr/lib", SandboxPath = "/usr/lib", ReadOnly = true }, + new DirectoryBinding { HostPath = "/lib", SandboxPath = "/lib", ReadOnly = true }, + new DirectoryBinding { HostPath = "/opt/kotlinc", SandboxPath = "/opt/kotlinc", ReadOnly = true } + } + }); + + var compilerOutput = string.Empty; + if (File.Exists(stderrFilePath)) + { + compilerOutput = await File.ReadAllTextAsync(stderrFilePath); + } + if (!string.IsNullOrEmpty(isolateResult.ErrorOutput)) + { + compilerOutput = $"{compilerOutput}\n{isolateResult.ErrorOutput}".Trim(); + } + + if (isolateResult.TimeLimitExceeded) + { + _logger.LogWarning("Kotlin compilation time limit exceeded for box {BoxId}", boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation timeout: exceeded {CompilationTimeLimitSeconds}s limit", + CompilerOutput = compilerOutput + }; + } + + if (isolateResult.MemoryLimitExceeded) + { + _logger.LogWarning("Kotlin compilation memory limit exceeded for box {BoxId}", boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation memory limit exceeded: {CompilationMemoryLimitMb}MB", + CompilerOutput = compilerOutput + }; + } + + if (isolateResult.ExitCode == 0 && File.Exists(boxJarPath)) + { + Directory.CreateDirectory(Path.GetDirectoryName(jarFilePath)!); + File.Copy(boxJarPath, jarFilePath, overwrite: true); + + _logger.LogInformation("Kotlin compilation successful in Isolate box {BoxId}", boxId); + return new CompilationResult + { + Success = true, + ExecutablePath = jarFilePath, + CompilerOutput = compilerOutput + }; + } + + _logger.LogWarning("Kotlin compilation failed with exit code {ExitCode} in box {BoxId}", + isolateResult.ExitCode, boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation failed with exit code {isolateResult.ExitCode}", + CompilerOutput = compilerOutput + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Isolate Kotlin compilation"); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation error: {ex.Message}" + }; + } + finally + { + if (boxId >= 0) + { + try + { + await _isolateService.CleanupBoxAsync(boxId); + _logger.LogDebug("Cleaned up isolate box {BoxId}", boxId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to cleanup compilation box {BoxId}", boxId); + } + + _boxPool.ReleaseBox(boxId); + _logger.LogDebug("Released compilation box {BoxId} back to pool", boxId); + } + } + } + + private (string compiler, string compilerFlags) ResolveVersion(string? version) + { + if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase)) + { + var compiler = _configuration["Kotlin:Compiler"] ?? "kotlinc"; + var compilerFlags = _configuration["Kotlin:CompilerFlags"] ?? ""; + return (compiler, compilerFlags); + } + + 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 ?? ""); + } + + _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); + } +} diff --git a/src/LiquidCode.Tester.Worker/Services/KotlinExecutionServiceIsolate.cs b/src/LiquidCode.Tester.Worker/Services/KotlinExecutionServiceIsolate.cs new file mode 100644 index 0000000..a95c686 --- /dev/null +++ b/src/LiquidCode.Tester.Worker/Services/KotlinExecutionServiceIsolate.cs @@ -0,0 +1,169 @@ +using System.Diagnostics; +using LiquidCode.Tester.Worker.Services.Isolate; + +namespace LiquidCode.Tester.Worker.Services; + +/// +/// Kotlin program execution service using Isolate sandbox +/// +public class KotlinExecutionServiceIsolate : IExecutionService +{ + private readonly ILogger _logger; + private readonly IsolateService _isolateService; + private readonly IsolateBoxPool _boxPool; + + public KotlinExecutionServiceIsolate( + ILogger logger, + IsolateService isolateService, + IsolateBoxPool boxPool) + { + _logger = logger; + _isolateService = isolateService; + _boxPool = boxPool; + } + + public async Task ExecuteAsync( + string executablePath, + string inputFilePath, + int timeLimitMs, + int memoryLimitMb) + { + _logger.LogInformation( + "Executing Kotlin JAR with Isolate: {Executable}, time={TimeLimit}ms, memory={MemoryLimit}MB", + executablePath, timeLimitMs, memoryLimitMb); + + var result = new ExecutionResult(); + var stopwatch = Stopwatch.StartNew(); + int boxId = -1; + + try + { + // Acquire a box from pool + boxId = await _boxPool.AcquireBoxAsync(); + _logger.LogDebug("Acquired isolate box {BoxId}", boxId); + + // Initialize the box + await _isolateService.InitBoxAsync(boxId); + + // Copy JAR file to box + var boxDir = $"/var/local/lib/isolate/{boxId}/box"; + var jarName = Path.GetFileName(executablePath); + var boxJarPath = Path.Combine(boxDir, jarName); + + File.Copy(executablePath, boxJarPath, overwrite: true); + + // Prepare output file in box + var outputFilePath = Path.Combine(boxDir, "output.txt"); + + // Run in Isolate (Kotlin runs via Java) + var kotlinMemoryMb = Math.Max(memoryLimitMb, 128); // Minimum 128MB for JVM + + var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions + { + BoxId = boxId, + Executable = "/usr/bin/java", + Arguments = new[] { "-jar", $"/box/{jarName}" }, + TimeLimitSeconds = timeLimitMs / 1000.0, + WallTimeLimitSeconds = (timeLimitMs / 1000.0) * 2, + MemoryLimitKb = kotlinMemoryMb * 1024, + StackLimitKb = 256 * 1024, + ProcessLimit = 64, // JVM creates multiple threads + EnableNetwork = false, + StdinFile = inputFilePath, + StdoutFile = outputFilePath, + WorkingDirectory = "/box" + }); + + stopwatch.Stop(); + + // Read output + if (File.Exists(outputFilePath)) + { + result.Output = await File.ReadAllTextAsync(outputFilePath); + } + + // Map Isolate result to ExecutionResult + result.ExecutionTimeMs = (long)(isolateResult.CpuTimeSeconds * 1000); + result.MemoryUsedMb = isolateResult.MemoryUsedKb / 1024; + result.ErrorOutput = isolateResult.ErrorOutput; + result.ExitCode = isolateResult.ExitCode; + + if (isolateResult.TimeLimitExceeded) + { + result.TimeLimitExceeded = true; + result.ErrorMessage = $"Time limit exceeded: {isolateResult.CpuTimeSeconds:F3}s"; + _logger.LogWarning("Time limit exceeded for box {BoxId}", boxId); + } + else if (isolateResult.MemoryLimitExceeded) + { + result.MemoryLimitExceeded = true; + result.ErrorMessage = $"Memory limit exceeded: {isolateResult.MemoryUsedKb / 1024}MB"; + if (isolateResult.CgroupOomKilled) + { + result.ErrorMessage += " (OOM killed by cgroup)"; + } + _logger.LogWarning("Memory limit exceeded for box {BoxId}", boxId); + } + else if (isolateResult.RuntimeError) + { + result.RuntimeError = true; + result.ErrorMessage = $"Runtime error: {isolateResult.Message}"; + if (isolateResult.ExitSignal.HasValue) + { + result.ErrorMessage += $" (signal {isolateResult.ExitSignal})"; + } + _logger.LogWarning("Runtime error for box {BoxId}: {Message}", boxId, isolateResult.Message); + } + else if (isolateResult.ExitCode == 0) + { + result.Success = true; + _logger.LogInformation( + "Execution successful: time={Time}ms, memory={Memory}MB, box={BoxId}", + result.ExecutionTimeMs, result.MemoryUsedMb, boxId); + } + else + { + result.RuntimeError = true; + result.ErrorMessage = $"Non-zero exit code: {isolateResult.ExitCode}"; + _logger.LogWarning("Non-zero exit code {ExitCode} for box {BoxId}", + isolateResult.ExitCode, boxId); + } + + _logger.LogDebug( + "Isolate stats: CPU={Cpu}s, Wall={Wall}s, Memory={Mem}KB", + isolateResult.CpuTimeSeconds, + isolateResult.WallTimeSeconds, + isolateResult.MemoryUsedKb); + } + catch (Exception ex) + { + stopwatch.Stop(); + result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds; + result.RuntimeError = true; + result.ErrorMessage = $"Execution error: {ex.Message}"; + _logger.LogError(ex, "Error during Isolate execution"); + } + finally + { + // Cleanup box + if (boxId >= 0) + { + try + { + await _isolateService.CleanupBoxAsync(boxId); + _logger.LogDebug("Cleaned up isolate box {BoxId}", boxId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to cleanup box {BoxId}", boxId); + } + + // Release box back to pool + _boxPool.ReleaseBox(boxId); + _logger.LogDebug("Released box {BoxId} back to pool", boxId); + } + } + + return result; + } +} diff --git a/src/LiquidCode.Tester.Worker/Services/OutputCheckerService.cs b/src/LiquidCode.Tester.Worker/Services/OutputCheckerService.cs index f209899..401a188 100644 --- a/src/LiquidCode.Tester.Worker/Services/OutputCheckerService.cs +++ b/src/LiquidCode.Tester.Worker/Services/OutputCheckerService.cs @@ -1,14 +1,21 @@ namespace LiquidCode.Tester.Worker.Services; +/// +/// Output checker service - always uses Isolate sandbox for checker execution +/// public class OutputCheckerService : IOutputCheckerService { private readonly ILogger _logger; - private readonly CheckerService _checkerService; + private readonly CheckerServiceIsolate _checkerService; - public OutputCheckerService(ILogger logger, CheckerService checkerService) + public OutputCheckerService( + ILogger logger, + CheckerServiceIsolate checkerService) { _logger = logger; _checkerService = checkerService; + + _logger.LogInformation("Checker service configured to use Isolate sandbox"); } public async Task CheckOutputAsync(string actualOutput, string expectedOutputPath) @@ -51,6 +58,12 @@ public class OutputCheckerService : IOutputCheckerService lines.RemoveAt(lines.Count - 1); } + // Remove leading empty lines + while (lines.Count > 0 && string.IsNullOrWhiteSpace(lines[0])) + { + lines.RemoveAt(0); + } + return string.Join("\n", lines); } @@ -63,13 +76,10 @@ public class OutputCheckerService : IOutputCheckerService // If custom checker is available, use it if (!string.IsNullOrEmpty(checkerPath) && File.Exists(checkerPath)) { - _logger.LogDebug("Using custom checker: {CheckerPath}", checkerPath); + _logger.LogDebug("Using custom checker in Isolate: {CheckerPath}", checkerPath); var checkerResult = await _checkerService.CheckAsync( - checkerPath, - inputFilePath, - actualOutput, - expectedOutputPath); + checkerPath, inputFilePath, actualOutput, expectedOutputPath); if (!checkerResult.Accepted) { diff --git a/src/LiquidCode.Tester.Worker/Services/PackageParserService.cs b/src/LiquidCode.Tester.Worker/Services/PackageParserService.cs index 35d330d..5a5cbf1 100644 --- a/src/LiquidCode.Tester.Worker/Services/PackageParserService.cs +++ b/src/LiquidCode.Tester.Worker/Services/PackageParserService.cs @@ -1,4 +1,10 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; using LiquidCode.Tester.Common.Models; namespace LiquidCode.Tester.Worker.Services; @@ -8,13 +14,13 @@ public class PackageParserService : IPackageParserService private readonly ILogger _logger; private readonly PolygonProblemXmlParser _polygonParser; private readonly AnswerGenerationService _answerGenerator; - private readonly CppCompilationService _cppCompilation; + private readonly CppCompilationServiceIsolate _cppCompilation; public PackageParserService( ILogger logger, PolygonProblemXmlParser polygonParser, AnswerGenerationService answerGenerator, - CppCompilationService cppCompilation) + CppCompilationServiceIsolate cppCompilation) { _logger = logger; _polygonParser = polygonParser; @@ -35,17 +41,22 @@ 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)) + // Search for problem.xml (Polygon package format is required) + var problemXmlPath = Directory.EnumerateFiles(workingDirectory, "problem.xml", SearchOption.AllDirectories) + .FirstOrDefault(); + + if (string.IsNullOrEmpty(problemXmlPath)) { - _logger.LogInformation("Detected Polygon package format (problem.xml found)"); - return await ParsePolygonPackageAsync(workingDirectory, problemXmlPath); + _logger.LogError("problem.xml not found in package. Only Polygon format is supported."); + throw new InvalidOperationException( + "Invalid package format: problem.xml not found. " + + "Only Polygon package format is supported. " + + "Please ensure your package contains a problem.xml file."); } - // Fall back to legacy format (.in/.out files) - _logger.LogInformation("Using legacy package format (.in/.out files)"); - return await ParseLegacyPackage(workingDirectory); + var packageRoot = Path.GetDirectoryName(problemXmlPath)!; + _logger.LogInformation("Polygon package detected (problem.xml found at {ProblemXml})", problemXmlPath); + return await ParsePolygonPackageAsync(packageRoot, problemXmlPath, workingDirectory); } catch (Exception ex) { @@ -54,89 +65,132 @@ public class PackageParserService : IPackageParserService } } - private async Task ParsePolygonPackageAsync(string workingDirectory, string problemXmlPath) + private async Task ParsePolygonPackageAsync(string packageRoot, string problemXmlPath, string extractionRoot) { var descriptor = _polygonParser.ParseProblemXml(problemXmlPath); if (descriptor == null) { - _logger.LogWarning("Failed to parse problem.xml, falling back to legacy format"); - return await ParseLegacyPackage(workingDirectory); + _logger.LogError("Failed to parse problem.xml"); + throw new InvalidOperationException( + "Failed to parse problem.xml. The Polygon package format may be corrupted or invalid."); + } + + // Check for interactive problems + if (descriptor.Interactor != null) + { + _logger.LogError("Interactive problem detected: {ShortName}", descriptor.ShortName); + throw new NotSupportedException( + "Interactive problems are not currently supported. " + + "The problem package contains an interactor, which is used for interactive tasks."); } var package = new ProblemPackage { - WorkingDirectory = workingDirectory, + WorkingDirectory = packageRoot, + ExtractionRoot = extractionRoot, DefaultTimeLimit = descriptor.TimeLimitMs, DefaultMemoryLimit = descriptor.MemoryLimitMb }; - // Collect test file paths and check which answers are missing - var inputPaths = new List(); - var answerPaths = new List(); - var missingAnswerPaths = new List(); - var missingAnswerInputs = new List(); + var buildDirectory = Path.Combine(packageRoot, ".lc-build"); + Directory.CreateDirectory(buildDirectory); - for (int i = 1; i <= descriptor.TestCount; i++) + var compiledExecutables = await CompilePolygonExecutablesAsync(descriptor, packageRoot, buildDirectory); + + package.CheckerPath = await CompileCheckerForPackageAsync(descriptor, packageRoot, buildDirectory) + ?? await FindAndCompileCheckerAsync(packageRoot); + + var validatorPath = await CompileValidatorAsync(descriptor, packageRoot, buildDirectory, compiledExecutables); + + await GenerateAndValidateTestsAsync(descriptor, packageRoot, compiledExecutables, validatorPath); + + var testIndices = descriptor.Tests.Count > 0 + ? descriptor.Tests.Select(t => t.Index).Distinct().OrderBy(i => i).ToList() + : Enumerable.Range(1, descriptor.TestCount > 0 ? descriptor.TestCount : 0).ToList(); + + if (testIndices.Count == 0) { - 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)); + _logger.LogError("No test definitions found in problem.xml"); + throw new InvalidOperationException( + "No test definitions found in problem.xml. " + + "The Polygon package must define tests in the problem.xml file."); + } - if (!File.Exists(inputPath)) + var inputs = new List<(int index, string inputPath)>(); + var answers = new Dictionary(); + + foreach (var testIndex in testIndices) + { + var inputRelative = FormatPolygonPattern(descriptor.InputPathPattern, testIndex); + var inputFullPath = Path.Combine(packageRoot, NormalizeRelativePath(inputRelative)); + + if (!File.Exists(inputFullPath)) { - _logger.LogWarning("Input file not found: {InputPath}", inputPath); + _logger.LogWarning("Input file not found for test {Index}: {RelativePath}", testIndex, inputRelative); continue; } - inputPaths.Add(inputPath); - answerPaths.Add(answerPath); + var answerRelative = FormatPolygonPattern(descriptor.AnswerPathPattern, testIndex); + var answerFullPath = Path.Combine(packageRoot, NormalizeRelativePath(answerRelative)); + + inputs.Add((testIndex, inputFullPath)); + answers[testIndex] = answerFullPath; + } + + var missingAnswerInputs = new List(); + var missingAnswerPaths = new List(); + + foreach (var (index, inputPath) in inputs) + { + if (!answers.TryGetValue(index, out var answerPath)) + { + continue; + } if (!File.Exists(answerPath)) { - missingAnswerPaths.Add(answerPath); missingAnswerInputs.Add(inputPath); + missingAnswerPaths.Add(answerPath); } } - // 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); + _logger.LogInformation("Found {Count} tests without answers, attempting to generate them", missingAnswerPaths.Count); var generated = await _answerGenerator.GenerateAnswersAsync( descriptor, - workingDirectory, + packageRoot, missingAnswerInputs, missingAnswerPaths); - if (generated) + if (!generated) { - _logger.LogInformation("Successfully generated answer files"); + _logger.LogWarning("Failed to generate answer files, affected tests will be skipped"); } else { - _logger.LogWarning("Failed to generate answer files, tests without answers will be skipped"); + _logger.LogInformation("Answer files generated successfully"); } } - // Now create test cases for all tests that have answer files - for (int i = 0; i < inputPaths.Count; i++) + foreach (var (index, inputPath) in inputs.OrderBy(item => item.index)) { - var inputPath = inputPaths[i]; - var answerPath = answerPaths[i]; + if (!answers.TryGetValue(index, out var answerPath)) + { + continue; + } if (!File.Exists(answerPath)) { - _logger.LogWarning("Answer file not found: {AnswerPath} (skipping test)", answerPath); + _logger.LogWarning("Answer file not found for test {Index}: {AnswerPath}", index, answerPath); continue; } package.TestCases.Add(new TestCase { - Number = i + 1, + Number = index, InputFilePath = inputPath, OutputFilePath = answerPath, TimeLimit = descriptor.TimeLimitMs, @@ -144,106 +198,460 @@ public class PackageParserService : IPackageParserService }); } - // 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.LogWarning("No test cases with answer files found for Polygon package"); } - _logger.LogInformation("Parsed Polygon package with {TestCount} tests (out of {TotalTests} in problem.xml)", + _logger.LogInformation("Parsed Polygon package with {TestCount} tests (declared {TotalTests})", package.TestCases.Count, descriptor.TestCount); return package; } - private async Task ParseLegacyPackage(string workingDirectory) + + private async Task> CompilePolygonExecutablesAsync( + PolygonProblemDescriptor descriptor, + string packageRoot, + string buildDirectory) { - var package = new ProblemPackage + var compiled = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var executable in descriptor.Executables) { - WorkingDirectory = workingDirectory - }; - - // Find tests directory - var testsDir = Path.Combine(workingDirectory, "tests"); - if (!Directory.Exists(testsDir)) - { - _logger.LogWarning("Tests directory not found, searching for test files in root"); - testsDir = workingDirectory; - } - - // Parse test cases - var inputFiles = Directory.GetFiles(testsDir, "*", SearchOption.AllDirectories) - .Where(f => Path.GetFileName(f).EndsWith(".in") || Path.GetFileName(f).Contains("input")) - .OrderBy(f => f) - .ToList(); - - for (int i = 0; i < inputFiles.Count; i++) - { - var inputFile = inputFiles[i]; - var outputFile = FindCorrespondingOutputFile(inputFile); - - if (outputFile == null) + if (string.IsNullOrWhiteSpace(executable.SourcePath)) { - _logger.LogWarning("No output file found for input {InputFile}", inputFile); continue; } - package.TestCases.Add(new TestCase + var alias = !string.IsNullOrWhiteSpace(executable.Name) + ? executable.Name + : Path.GetFileNameWithoutExtension(executable.SourcePath); + + if (string.IsNullOrWhiteSpace(alias)) { - Number = i + 1, - InputFilePath = inputFile, - OutputFilePath = outputFile, - TimeLimit = package.DefaultTimeLimit, - MemoryLimit = package.DefaultMemoryLimit - }); - } + continue; + } - // Look for and compile checker - package.CheckerPath = await FindAndCompileCheckerAsync(workingDirectory); + var sourceFullPath = Path.Combine(packageRoot, NormalizeRelativePath(executable.SourcePath)); - if (package.TestCases.Count == 0) - { - _logger.LogWarning("No test cases found! Check package structure. Expected .in/.out files in tests directory or root"); - } - - _logger.LogInformation("Parsed legacy package with {TestCount} tests", package.TestCases.Count); - return package; - } - - private string? FindCorrespondingOutputFile(string inputFile) - { - var directory = Path.GetDirectoryName(inputFile)!; - var fileName = Path.GetFileNameWithoutExtension(inputFile); - var extension = Path.GetExtension(inputFile); - - // Try various output file naming patterns - var patterns = new[] - { - fileName.Replace("input", "output") + ".out", - fileName.Replace("input", "output") + ".a", - fileName.Replace("input", "answer") + ".out", - fileName.Replace("input", "answer") + ".a", - fileName + ".out", - fileName + ".a", - fileName.Replace(".in", ".out"), - fileName.Replace(".in", ".a") - }; - - foreach (var pattern in patterns) - { - var candidate = Path.Combine(directory, pattern); - if (File.Exists(candidate)) + if (!File.Exists(sourceFullPath)) { - return candidate; + _logger.LogWarning("Executable source not found: {Path}", executable.SourcePath); + continue; + } + + if (IsCppAsset(executable.Type, sourceFullPath)) + { + var outputPath = Path.Combine(buildDirectory, alias); + var includeDirs = GetIncludeDirectories(packageRoot, sourceFullPath); + var stdVersion = ExtractCppStandard(executable.Type); + + var compilationResult = await _cppCompilation.CompileFileAsync( + sourceFullPath, + outputPath, + stdVersion, + includeDirs); + + if (compilationResult.Success && !string.IsNullOrEmpty(compilationResult.ExecutablePath)) + { + _logger.LogInformation("Compiled executable {Alias} -> {Path}", alias, compilationResult.ExecutablePath); + compiled[alias] = compilationResult.ExecutablePath; + } + else + { + _logger.LogWarning("Failed to compile executable {Alias}: {Error}", alias, compilationResult.CompilerOutput); + } + } + else + { + var binaryPath = !string.IsNullOrWhiteSpace(executable.BinaryPath) + ? Path.Combine(packageRoot, NormalizeRelativePath(executable.BinaryPath)) + : null; + + if (!string.IsNullOrEmpty(binaryPath) && File.Exists(binaryPath)) + { + _logger.LogInformation("Using prebuilt executable {Alias} at {Path}", alias, binaryPath); + compiled[alias] = binaryPath; + } + else + { + _logger.LogWarning("Unsupported executable type {Type} for {Alias}", executable.Type, alias); + } } } + return compiled; + } + + private async Task CompileCheckerForPackageAsync( + PolygonProblemDescriptor descriptor, + string packageRoot, + string buildDirectory) + { + if (descriptor.Checker == null || string.IsNullOrWhiteSpace(descriptor.Checker.SourcePath)) + { + _logger.LogInformation("No checker declared in problem.xml"); + return null; + } + + var sourcePath = Path.Combine(packageRoot, NormalizeRelativePath(descriptor.Checker.SourcePath)); + + if (!File.Exists(sourcePath)) + { + _logger.LogWarning("Checker source not found: {SourcePath}", descriptor.Checker.SourcePath); + return null; + } + + var alias = !string.IsNullOrWhiteSpace(descriptor.Checker.CopyPath) + ? Path.GetFileNameWithoutExtension(descriptor.Checker.CopyPath) + : Path.GetFileNameWithoutExtension(descriptor.Checker.BinaryPath ?? descriptor.Checker.SourcePath); + + if (string.IsNullOrWhiteSpace(alias)) + { + alias = "checker"; + } + + var outputPath = Path.Combine(buildDirectory, alias); + var includeDirs = GetIncludeDirectories(packageRoot, sourcePath); + var stdVersion = ExtractCppStandard(descriptor.Checker.Type); + + var result = await _cppCompilation.CompileFileAsync( + sourcePath, + outputPath, + stdVersion, + includeDirs); + + if (result.Success && !string.IsNullOrEmpty(result.ExecutablePath)) + { + _logger.LogInformation("Checker compiled to {Path}", result.ExecutablePath); + return result.ExecutablePath; + } + + _logger.LogWarning("Failed to compile checker: {Error}", result.CompilerOutput); return null; } + private async Task CompileValidatorAsync( + PolygonProblemDescriptor descriptor, + string packageRoot, + string buildDirectory, + IDictionary compiledExecutables) + { + var validator = descriptor.Validators.FirstOrDefault(); + if (validator == null || string.IsNullOrWhiteSpace(validator.SourcePath)) + { + _logger.LogInformation("No validator declared in problem.xml"); + return null; + } + + var sourcePath = Path.Combine(packageRoot, NormalizeRelativePath(validator.SourcePath)); + if (!File.Exists(sourcePath)) + { + _logger.LogWarning("Validator source not found: {SourcePath}", validator.SourcePath); + return null; + } + + var alias = Path.GetFileNameWithoutExtension(validator.BinaryPath ?? validator.SourcePath); + if (string.IsNullOrWhiteSpace(alias)) + { + alias = "validator"; + } + + if (compiledExecutables.TryGetValue(alias, out var compiledPath) && File.Exists(compiledPath)) + { + _logger.LogInformation("Reusing precompiled validator executable at {Path}", compiledPath); + return compiledPath; + } + + var outputPath = Path.Combine(buildDirectory, alias); + var includeDirs = GetIncludeDirectories(packageRoot, sourcePath); + var stdVersion = ExtractCppStandard(validator.Type); + + var result = await _cppCompilation.CompileFileAsync( + sourcePath, + outputPath, + stdVersion, + includeDirs); + + if (result.Success && !string.IsNullOrEmpty(result.ExecutablePath)) + { + _logger.LogInformation("Validator compiled to {Path}", result.ExecutablePath); + compiledExecutables[alias] = result.ExecutablePath; + return result.ExecutablePath; + } + + _logger.LogWarning("Failed to compile validator: {Error}", result.CompilerOutput); + return null; + } + + private async Task GenerateAndValidateTestsAsync( + PolygonProblemDescriptor descriptor, + string packageRoot, + IDictionary compiledExecutables, + string? validatorPath) + { + if (descriptor.Tests.Count == 0) + { + _logger.LogInformation("problem.xml does not enumerate tests; skipping generation step"); + return; + } + + foreach (var test in descriptor.Tests) + { + var inputRelative = FormatPolygonPattern(descriptor.InputPathPattern, test.Index); + var inputFullPath = Path.Combine(packageRoot, NormalizeRelativePath(inputRelative)); + + if (test.Method is PolygonTestMethod.Generated or PolygonTestMethod.Script) + { + await GenerateTestInputAsync(test, packageRoot, inputFullPath, compiledExecutables); + } + else if (!File.Exists(inputFullPath)) + { + _logger.LogWarning("Manual test {Index} expected at {Path} but not found", test.Index, inputRelative); + continue; + } + + if (!File.Exists(inputFullPath)) + { + throw new InvalidOperationException($"Failed to produce input file for test {test.Index} ({inputRelative})"); + } + + if (!string.IsNullOrEmpty(validatorPath)) + { + await ValidateInputAsync(validatorPath, inputFullPath, test.Index); + } + } + } + + private async Task GenerateTestInputAsync( + PolygonTestDefinition test, + string packageRoot, + string inputFullPath, + IDictionary compiledExecutables) + { + if (string.IsNullOrWhiteSpace(test.Command)) + { + _logger.LogWarning("Test {Index} is marked as generated but has no command", test.Index); + return; + } + + var args = SplitCommandLine(test.Command); + if (args.Count == 0) + { + _logger.LogWarning("Failed to parse generator command for test {Index}", test.Index); + return; + } + + var generatorAlias = args[0]; + + if (!compiledExecutables.TryGetValue(generatorAlias, out var generatorPath)) + { + _logger.LogWarning("Generator {Generator} not found for test {Index}", generatorAlias, test.Index); + return; + } + + Directory.CreateDirectory(Path.GetDirectoryName(inputFullPath)!); + + _logger.LogInformation("Generating test {Index} using {Generator} {Arguments}", + test.Index, + generatorAlias, + string.Join(' ', args.Skip(1))); + + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = generatorPath, + WorkingDirectory = packageRoot, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + foreach (var argument in args.Skip(1)) + { + process.StartInfo.ArgumentList.Add(argument); + } + + process.Start(); + + var stderrTask = process.StandardError.ReadToEndAsync(); + + await using (var outputStream = File.Create(inputFullPath)) + { + await process.StandardOutput.BaseStream.CopyToAsync(outputStream); + } + + await process.WaitForExitAsync(); + var stderr = await stderrTask; + + if (process.ExitCode != 0) + { + throw new InvalidOperationException($"Generator '{generatorAlias}' failed for test {test.Index}: {stderr}"); + } + + if (!string.IsNullOrWhiteSpace(stderr)) + { + _logger.LogDebug("Generator '{Generator}' stderr for test {Index}: {Message}", generatorAlias, test.Index, stderr.Trim()); + } + } + + private async Task ValidateInputAsync(string validatorPath, string inputFilePath, int testIndex) + { + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = validatorPath, + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + process.Start(); + + var inputContent = await File.ReadAllTextAsync(inputFilePath); + var normalizedInput = inputContent.Replace("\r\n", "\n").Replace("\r", "\n"); + await process.StandardInput.WriteAsync(normalizedInput); + process.StandardInput.Close(); + + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + + await process.WaitForExitAsync(); + + var stdout = await stdoutTask; + var stderr = await stderrTask; + + if (process.ExitCode != 0) + { + var message = string.IsNullOrWhiteSpace(stderr) ? stdout : stderr; + throw new InvalidOperationException($"Validator rejected test {testIndex}: {message?.Trim()}"); + } + + if (!string.IsNullOrWhiteSpace(stdout)) + { + _logger.LogDebug("Validator output for test {Index}: {Message}", testIndex, stdout.Trim()); + } + } + + private static IEnumerable GetIncludeDirectories(string packageRoot, string sourceFullPath) + { + var sourceDirectory = Path.GetDirectoryName(sourceFullPath); + var filesDirectory = Path.Combine(packageRoot, "files"); + + return new[] { sourceDirectory, filesDirectory, packageRoot } + .Where(dir => !string.IsNullOrWhiteSpace(dir)) + .Select(dir => dir!) + .Where(Directory.Exists) + .Distinct(); + } + + private static string NormalizeRelativePath(string relativePath) + { + return relativePath + .Replace("\\", Path.DirectorySeparatorChar.ToString(CultureInfo.InvariantCulture)) + .Replace("/", Path.DirectorySeparatorChar.ToString(CultureInfo.InvariantCulture)); + } + + private static string FormatPolygonPattern(string pattern, int index) + { + if (string.IsNullOrWhiteSpace(pattern)) + { + return index.ToString(CultureInfo.InvariantCulture); + } + + return Regex.Replace(pattern, "%0?(\\d*)d", match => + { + if (int.TryParse(match.Groups[1].Value, out var width) && width > 0) + { + return index.ToString($"D{width}", CultureInfo.InvariantCulture); + } + + return index.ToString(CultureInfo.InvariantCulture); + }); + } + + private static IReadOnlyList SplitCommandLine(string command) + { + var result = new List(); + if (string.IsNullOrWhiteSpace(command)) + { + return result; + } + + var current = new StringBuilder(); + var inQuotes = false; + + foreach (var ch in command) + { + if (ch == '"') + { + inQuotes = !inQuotes; + continue; + } + + if (char.IsWhiteSpace(ch) && !inQuotes) + { + if (current.Length > 0) + { + result.Add(current.ToString()); + current.Clear(); + } + } + else + { + current.Append(ch); + } + } + + if (current.Length > 0) + { + result.Add(current.ToString()); + } + + return result; + } + + private static bool IsCppAsset(string? type, string sourceFullPath) + { + if (!string.IsNullOrWhiteSpace(type) && type.StartsWith("cpp", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + var extension = Path.GetExtension(sourceFullPath); + return extension.Equals(".cpp", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".cc", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".cxx", StringComparison.OrdinalIgnoreCase); + } + + private static string? ExtractCppStandard(string? type) + { + if (string.IsNullOrWhiteSpace(type)) + { + return null; + } + + var normalized = type.ToLowerInvariant(); + + if (normalized.Contains("++26")) return "26"; + if (normalized.Contains("++23")) return "23"; + if (normalized.Contains("++20")) return "20"; + if (normalized.Contains("++17")) return "17"; + if (normalized.Contains("++14")) return "14"; + if (normalized.Contains("++11")) return "11"; + + return null; + } + + private async Task FindAndCompileCheckerAsync(string workingDirectory) { // Try to find checker in common locations @@ -278,14 +686,16 @@ public class PackageParserService : IPackageParserService try { - var checkerSource = await File.ReadAllTextAsync(candidate); var checkerDir = Path.GetDirectoryName(candidate)!; + var outputPath = Path.Combine(checkerDir, Path.GetFileNameWithoutExtension(candidate)); + var includeDirs = GetIncludeDirectories(workingDirectory, candidate); - // Compile checker with C++17 (testlib.h compatible) - var compilationResult = await _cppCompilation.CompileAsync( - checkerSource, - checkerDir, - "17"); + // Compile checker with inferred standard (default C++17) + var compilationResult = await _cppCompilation.CompileFileAsync( + candidate, + outputPath, + "17", + includeDirs); if (!compilationResult.Success) { diff --git a/src/LiquidCode.Tester.Worker/Services/PolygonProblemXmlParser.cs b/src/LiquidCode.Tester.Worker/Services/PolygonProblemXmlParser.cs index cef0102..a765c3f 100644 --- a/src/LiquidCode.Tester.Worker/Services/PolygonProblemXmlParser.cs +++ b/src/LiquidCode.Tester.Worker/Services/PolygonProblemXmlParser.cs @@ -1,3 +1,6 @@ +using System.Globalization; +using System.IO; +using System.Linq; using System.Xml.Linq; using LiquidCode.Tester.Common.Models; @@ -73,7 +76,89 @@ public class PolygonProblemXmlParser 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 + // Parse detailed test definitions (if present) + var testsElement = testset.Element("tests"); + if (testsElement != null) + { + var index = 1; + foreach (var testElement in testsElement.Elements("test")) + { + var methodValue = testElement.Attribute("method")?.Value ?? "manual"; + var method = methodValue.ToLowerInvariant() switch + { + "generated" => PolygonTestMethod.Generated, + "manual" => PolygonTestMethod.Manual, + "script" => PolygonTestMethod.Script, + _ => PolygonTestMethod.Unknown + }; + + double? points = null; + var pointsAttr = testElement.Attribute("points")?.Value; + if (!string.IsNullOrWhiteSpace(pointsAttr) && + double.TryParse(pointsAttr, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedPoints)) + { + points = parsedPoints; + } + + var definition = new PolygonTestDefinition + { + Index = index, + Method = method, + Command = testElement.Attribute("cmd")?.Value, + Group = testElement.Attribute("group")?.Value, + IsSample = bool.TryParse(testElement.Attribute("sample")?.Value, out var sample) && sample, + Points = points, + Comment = testElement.Attribute("comment")?.Value + }; + + descriptor.Tests.Add(definition); + index++; + } + + if (descriptor.TestCount == 0) + { + descriptor.TestCount = descriptor.Tests.Count; + } + } + + // Parse auxiliary executables defined in + var filesSection = problem.Element("files"); + if (filesSection != null) + { + var executablesSection = filesSection.Element("executables"); + if (executablesSection != null) + { + foreach (var executableElement in executablesSection.Elements("executable")) + { + var sourceElement = executableElement.Element("source"); + if (sourceElement == null) + { + continue; + } + + var sourcePath = sourceElement.Attribute("path")?.Value; + if (string.IsNullOrWhiteSpace(sourcePath)) + { + continue; + } + + var binaryPath = executableElement.Element("binary")?.Attribute("path")?.Value; + var name = !string.IsNullOrWhiteSpace(binaryPath) + ? Path.GetFileNameWithoutExtension(binaryPath) + : Path.GetFileNameWithoutExtension(sourcePath); + + descriptor.Executables.Add(new PolygonExecutableDescriptor + { + Name = name, + SourcePath = sourcePath, + BinaryPath = binaryPath, + Type = sourceElement.Attribute("type")?.Value + }); + } + } + } + + // Parse assets: solutions, checker, validators var assets = problem.Element("assets"); if (assets != null) { @@ -108,6 +193,59 @@ public class PolygonProblemXmlParser _logger.LogWarning("No main or accepted solution found in problem.xml"); } } + + var checkerElement = assets.Element("checker"); + if (checkerElement != null) + { + var checkerSource = checkerElement.Element("source"); + if (checkerSource != null) + { + descriptor.Checker = new PolygonCheckerDescriptor + { + SourcePath = checkerSource.Attribute("path")?.Value ?? string.Empty, + Type = checkerSource.Attribute("type")?.Value, + BinaryPath = checkerElement.Element("binary")?.Attribute("path")?.Value, + CopyPath = checkerElement.Element("copy")?.Attribute("path")?.Value + }; + } + } + + var interactorElement = assets.Element("interactor"); + if (interactorElement != null) + { + var interactorSource = interactorElement.Element("source"); + if (interactorSource != null) + { + descriptor.Interactor = new PolygonInteractorDescriptor + { + SourcePath = interactorSource.Attribute("path")?.Value ?? string.Empty, + Type = interactorSource.Attribute("type")?.Value, + BinaryPath = interactorElement.Element("binary")?.Attribute("path")?.Value + }; + } + } + + var validatorsSection = assets.Element("validators"); + if (validatorsSection != null) + { + foreach (var validatorElement in validatorsSection.Elements("validator")) + { + var validatorSource = validatorElement.Element("source"); + if (validatorSource == null) + { + continue; + } + + var validator = new PolygonValidatorDescriptor + { + SourcePath = validatorSource.Attribute("path")?.Value ?? string.Empty, + Type = validatorSource.Attribute("type")?.Value, + BinaryPath = validatorElement.Element("binary")?.Attribute("path")?.Value + }; + + descriptor.Validators.Add(validator); + } + } } _logger.LogInformation( @@ -138,4 +276,73 @@ public class PolygonProblemDescriptor public string AnswerPathPattern { get; set; } = "tests/%02d.a"; public string? MainSolutionPath { get; set; } public string? MainSolutionType { get; set; } + public List Tests { get; } = new(); + public List Executables { get; } = new(); + public PolygonCheckerDescriptor? Checker { get; set; } + public PolygonInteractorDescriptor? Interactor { get; set; } + public List Validators { get; } = new(); +} + +/// +/// Represents a single test entry defined in problem.xml +/// +public class PolygonTestDefinition +{ + public int Index { get; set; } + public PolygonTestMethod Method { get; set; } = PolygonTestMethod.Manual; + public string? Command { get; set; } + public string? Group { get; set; } + public bool IsSample { get; set; } + public double? Points { get; set; } + public string? Comment { get; set; } +} + +public enum PolygonTestMethod +{ + Manual, + Generated, + Script, + Unknown +} + +/// +/// Represents additional executables (generators, validators, printers) declared in problem.xml +/// +public class PolygonExecutableDescriptor +{ + public string Name { get; set; } = string.Empty; + public string SourcePath { get; set; } = string.Empty; + public string? BinaryPath { get; set; } + public string? Type { get; set; } +} + +/// +/// Represents checker information declared in problem.xml +/// +public class PolygonCheckerDescriptor +{ + public string SourcePath { get; set; } = string.Empty; + public string? BinaryPath { get; set; } + public string? CopyPath { get; set; } + public string? Type { get; set; } +} + +/// +/// Represents interactor information declared in problem.xml +/// +public class PolygonInteractorDescriptor +{ + public string SourcePath { get; set; } = string.Empty; + public string? BinaryPath { get; set; } + public string? Type { get; set; } +} + +/// +/// Represents validator information declared in problem.xml +/// +public class PolygonValidatorDescriptor +{ + public string SourcePath { get; set; } = string.Empty; + public string? BinaryPath { get; set; } + public string? Type { get; set; } } diff --git a/src/LiquidCode.Tester.Worker/Services/PythonCompilationServiceIsolate.cs b/src/LiquidCode.Tester.Worker/Services/PythonCompilationServiceIsolate.cs new file mode 100644 index 0000000..d498877 --- /dev/null +++ b/src/LiquidCode.Tester.Worker/Services/PythonCompilationServiceIsolate.cs @@ -0,0 +1,221 @@ +using LiquidCode.Tester.Worker.Services.Isolate; +using LiquidCode.Tester.Worker.Models; + +namespace LiquidCode.Tester.Worker.Services; + +/// +/// Python compilation service using Isolate sandbox for security +/// Python doesn't require compilation, but performs syntax validation in sandbox +/// +public class PythonCompilationServiceIsolate : ICompilationService +{ + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + private readonly IsolateService _isolateService; + private readonly IsolateBoxPool _boxPool; + + private const int ValidationTimeLimitSeconds = 10; + private const int ValidationMemoryLimitMb = 256; + + public PythonCompilationServiceIsolate( + ILogger logger, + IConfiguration configuration, + IsolateService isolateService, + IsolateBoxPool boxPool) + { + _logger = logger; + _configuration = configuration; + _isolateService = isolateService; + _boxPool = boxPool; + } + + public async Task CompileAsync(string sourceCode, string workingDirectory, string? version = null) + { + var sourceFilePath = Path.Combine(workingDirectory, "solution.py"); + + _logger.LogInformation("Preparing Python code with Isolate validation in {WorkingDirectory} with version {Version}", + workingDirectory, version ?? "latest"); + + try + { + await File.WriteAllTextAsync(sourceFilePath, sourceCode); + + // Optionally validate syntax in Isolate sandbox + var validateSyntax = _configuration.GetValue("Python:ValidateSyntax", true); + if (validateSyntax) + { + var validationResult = await ValidateSyntaxInIsolateAsync(sourceFilePath, version); + if (!validationResult.Success) + { + return validationResult; + } + } + + var executable = ResolveVersion(version); + _logger.LogInformation("Python code prepared and validated successfully"); + + return new CompilationResult + { + Success = true, + ExecutablePath = sourceFilePath, + CompilerOutput = $"Python code validated successfully (using {executable})" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error preparing Python code"); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Error preparing code: {ex.Message}" + }; + } + } + + private async Task ValidateSyntaxInIsolateAsync( + string sourceFilePath, + string? version = null) + { + int boxId = -1; + + try + { + boxId = await _boxPool.AcquireBoxAsync(); + _logger.LogDebug("Acquired isolate box {BoxId} for Python syntax validation", boxId); + + await _isolateService.InitBoxAsync(boxId); + + var boxDir = $"/var/local/lib/isolate/{boxId}/box"; + var sourceFileName = Path.GetFileName(sourceFilePath); + var boxSourcePath = Path.Combine(boxDir, sourceFileName); + + File.Copy(sourceFilePath, boxSourcePath, overwrite: true); + + var executable = ResolveVersion(version); + _logger.LogDebug("Using Python executable: {Executable}", executable); + + var stderrFilePath = Path.Combine(boxDir, "validation_stderr.txt"); + + // Validate syntax using: python3 -m py_compile solution.py + var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions + { + BoxId = boxId, + Executable = $"/usr/bin/{executable}", + Arguments = new[] { "-m", "py_compile", $"/box/{sourceFileName}" }, + TimeLimitSeconds = ValidationTimeLimitSeconds, + WallTimeLimitSeconds = ValidationTimeLimitSeconds * 2, + MemoryLimitKb = ValidationMemoryLimitMb * 1024, + StackLimitKb = 64 * 1024, + ProcessLimit = 1, + EnableNetwork = false, + StderrFile = stderrFilePath, + WorkingDirectory = "/box", + DirectoryBindings = new List + { + new DirectoryBinding { HostPath = "/usr/lib", SandboxPath = "/usr/lib", ReadOnly = true }, + new DirectoryBinding { HostPath = "/lib", SandboxPath = "/lib", ReadOnly = true } + } + }); + + var validationOutput = string.Empty; + if (File.Exists(stderrFilePath)) + { + validationOutput = await File.ReadAllTextAsync(stderrFilePath); + } + if (!string.IsNullOrEmpty(isolateResult.ErrorOutput)) + { + validationOutput = $"{validationOutput}\n{isolateResult.ErrorOutput}".Trim(); + } + + if (isolateResult.TimeLimitExceeded) + { + _logger.LogWarning("Python validation time limit exceeded for box {BoxId}", boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Validation timeout: exceeded {ValidationTimeLimitSeconds}s limit", + CompilerOutput = validationOutput + }; + } + + if (isolateResult.MemoryLimitExceeded) + { + _logger.LogWarning("Python validation memory limit exceeded for box {BoxId}", boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Validation memory limit exceeded: {ValidationMemoryLimitMb}MB", + CompilerOutput = validationOutput + }; + } + + if (isolateResult.ExitCode != 0) + { + _logger.LogWarning("Python syntax validation failed with exit code {ExitCode} in box {BoxId}", + isolateResult.ExitCode, boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = "Python syntax error", + CompilerOutput = validationOutput + }; + } + + _logger.LogDebug("Python syntax validation successful in Isolate box {BoxId}", boxId); + return new CompilationResult + { + Success = true, + CompilerOutput = "Syntax validation passed" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Python syntax validation"); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Validation error: {ex.Message}" + }; + } + finally + { + if (boxId >= 0) + { + try + { + await _isolateService.CleanupBoxAsync(boxId); + _logger.LogDebug("Cleaned up isolate box {BoxId}", boxId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to cleanup validation box {BoxId}", boxId); + } + + _boxPool.ReleaseBox(boxId); + _logger.LogDebug("Released validation box {BoxId} back to pool", boxId); + } + } + } + + private string ResolveVersion(string? version) + { + if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase)) + { + var executable = _configuration["Python:Executable"] ?? "python3"; + return executable; + } + + var versionKey = $"Python:Versions:{version}"; + var versionExecutable = _configuration[$"{versionKey}:Executable"]; + + if (!string.IsNullOrEmpty(versionExecutable)) + { + _logger.LogInformation("Using Python version {Version} configuration", version); + return versionExecutable; + } + + _logger.LogWarning("Python version {Version} not found in configuration, using default", version); + var defaultExecutable = _configuration["Python:Executable"] ?? "python3"; + return defaultExecutable; + } +} diff --git a/src/LiquidCode.Tester.Worker/Services/PythonExecutionServiceIsolate.cs b/src/LiquidCode.Tester.Worker/Services/PythonExecutionServiceIsolate.cs new file mode 100644 index 0000000..78f3a01 --- /dev/null +++ b/src/LiquidCode.Tester.Worker/Services/PythonExecutionServiceIsolate.cs @@ -0,0 +1,173 @@ +using System.Diagnostics; +using LiquidCode.Tester.Worker.Services.Isolate; + +namespace LiquidCode.Tester.Worker.Services; + +/// +/// Python program execution service using Isolate sandbox +/// +public class PythonExecutionServiceIsolate : IExecutionService +{ + private readonly ILogger _logger; + private readonly IsolateService _isolateService; + private readonly IsolateBoxPool _boxPool; + private readonly IConfiguration _configuration; + + public PythonExecutionServiceIsolate( + ILogger logger, + IsolateService isolateService, + IsolateBoxPool boxPool, + IConfiguration configuration) + { + _logger = logger; + _isolateService = isolateService; + _boxPool = boxPool; + _configuration = configuration; + } + + public async Task ExecuteAsync( + string executablePath, + string inputFilePath, + int timeLimitMs, + int memoryLimitMb) + { + _logger.LogInformation( + "Executing Python with Isolate: {Executable}, time={TimeLimit}ms, memory={MemoryLimit}MB", + executablePath, timeLimitMs, memoryLimitMb); + + var result = new ExecutionResult(); + var stopwatch = Stopwatch.StartNew(); + int boxId = -1; + + try + { + // Acquire a box from pool + boxId = await _boxPool.AcquireBoxAsync(); + _logger.LogDebug("Acquired isolate box {BoxId}", boxId); + + // Initialize the box + await _isolateService.InitBoxAsync(boxId); + + // Copy Python script to box + var boxDir = $"/var/local/lib/isolate/{boxId}/box"; + var scriptName = Path.GetFileName(executablePath); + var boxScriptPath = Path.Combine(boxDir, scriptName); + + File.Copy(executablePath, boxScriptPath, overwrite: true); + + // Prepare output file in box + var outputFilePath = Path.Combine(boxDir, "output.txt"); + + // Get Python executable from configuration + var pythonExecutable = _configuration["Python:Executable"] ?? "python3"; + + // Run in Isolate + var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions + { + BoxId = boxId, + Executable = $"/usr/bin/{pythonExecutable}", + Arguments = new[] { $"/box/{scriptName}" }, + TimeLimitSeconds = timeLimitMs / 1000.0, + WallTimeLimitSeconds = (timeLimitMs / 1000.0) * 2, + MemoryLimitKb = memoryLimitMb * 1024, + StackLimitKb = 256 * 1024, + ProcessLimit = 1, // Single process for Python + EnableNetwork = false, + StdinFile = inputFilePath, + StdoutFile = outputFilePath, + WorkingDirectory = "/box" + }); + + stopwatch.Stop(); + + // Read output + if (File.Exists(outputFilePath)) + { + result.Output = await File.ReadAllTextAsync(outputFilePath); + } + + // Map Isolate result to ExecutionResult + result.ExecutionTimeMs = (long)(isolateResult.CpuTimeSeconds * 1000); + result.MemoryUsedMb = isolateResult.MemoryUsedKb / 1024; + result.ErrorOutput = isolateResult.ErrorOutput; + result.ExitCode = isolateResult.ExitCode; + + if (isolateResult.TimeLimitExceeded) + { + result.TimeLimitExceeded = true; + result.ErrorMessage = $"Time limit exceeded: {isolateResult.CpuTimeSeconds:F3}s"; + _logger.LogWarning("Time limit exceeded for box {BoxId}", boxId); + } + else if (isolateResult.MemoryLimitExceeded) + { + result.MemoryLimitExceeded = true; + result.ErrorMessage = $"Memory limit exceeded: {isolateResult.MemoryUsedKb / 1024}MB"; + if (isolateResult.CgroupOomKilled) + { + result.ErrorMessage += " (OOM killed by cgroup)"; + } + _logger.LogWarning("Memory limit exceeded for box {BoxId}", boxId); + } + else if (isolateResult.RuntimeError) + { + result.RuntimeError = true; + result.ErrorMessage = $"Runtime error: {isolateResult.Message}"; + if (isolateResult.ExitSignal.HasValue) + { + result.ErrorMessage += $" (signal {isolateResult.ExitSignal})"; + } + _logger.LogWarning("Runtime error for box {BoxId}: {Message}", boxId, isolateResult.Message); + } + else if (isolateResult.ExitCode == 0) + { + result.Success = true; + _logger.LogInformation( + "Execution successful: time={Time}ms, memory={Memory}MB, box={BoxId}", + result.ExecutionTimeMs, result.MemoryUsedMb, boxId); + } + else + { + result.RuntimeError = true; + result.ErrorMessage = $"Non-zero exit code: {isolateResult.ExitCode}"; + _logger.LogWarning("Non-zero exit code {ExitCode} for box {BoxId}", + isolateResult.ExitCode, boxId); + } + + _logger.LogDebug( + "Isolate stats: CPU={Cpu}s, Wall={Wall}s, Memory={Mem}KB", + isolateResult.CpuTimeSeconds, + isolateResult.WallTimeSeconds, + isolateResult.MemoryUsedKb); + } + catch (Exception ex) + { + stopwatch.Stop(); + result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds; + result.RuntimeError = true; + result.ErrorMessage = $"Execution error: {ex.Message}"; + _logger.LogError(ex, "Error during Isolate execution"); + } + finally + { + // Cleanup box + if (boxId >= 0) + { + try + { + await _isolateService.CleanupBoxAsync(boxId); + _logger.LogDebug("Cleaned up isolate box {BoxId}", boxId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to cleanup box {BoxId}", boxId); + } + + // Release box back to pool + _boxPool.ReleaseBox(boxId); + _logger.LogDebug("Released box {BoxId} back to pool", boxId); + } + } + + return result; + } +} diff --git a/src/LiquidCode.Tester.Worker/Services/TestingService.cs b/src/LiquidCode.Tester.Worker/Services/TestingService.cs index 9c05d43..db37283 100644 --- a/src/LiquidCode.Tester.Worker/Services/TestingService.cs +++ b/src/LiquidCode.Tester.Worker/Services/TestingService.cs @@ -64,7 +64,7 @@ public class TestingService : ITestingService _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); + CleanupWorkingDirectory(package.ExtractionRoot ?? package.WorkingDirectory); return; } @@ -113,7 +113,7 @@ public class TestingService : ITestingService _logger.LogWarning("Time limit exceeded on test {TestNumber}", testCase.Number); await SendStatusAsync(request, State.Done, ErrorCode.TimeLimitError, $"Time limit exceeded on test {testCase.Number}", testCase.Number, package.TestCases.Count); - CleanupWorkingDirectory(package.WorkingDirectory); + CleanupWorkingDirectory(package.ExtractionRoot ?? package.WorkingDirectory); return; } @@ -122,7 +122,7 @@ public class TestingService : ITestingService _logger.LogWarning("Memory limit exceeded on test {TestNumber}", testCase.Number); await SendStatusAsync(request, State.Done, ErrorCode.MemoryError, $"Memory limit exceeded on test {testCase.Number}", testCase.Number, package.TestCases.Count); - CleanupWorkingDirectory(package.WorkingDirectory); + CleanupWorkingDirectory(package.ExtractionRoot ?? package.WorkingDirectory); return; } @@ -131,7 +131,7 @@ public class TestingService : ITestingService _logger.LogWarning("Runtime error on test {TestNumber}: {Error}", testCase.Number, executionResult.ErrorMessage); await SendStatusAsync(request, State.Done, ErrorCode.RuntimeError, $"Runtime error on test {testCase.Number}: {executionResult.ErrorMessage}", testCase.Number, package.TestCases.Count); - CleanupWorkingDirectory(package.WorkingDirectory); + CleanupWorkingDirectory(package.ExtractionRoot ?? package.WorkingDirectory); return; } @@ -147,7 +147,7 @@ public class TestingService : ITestingService _logger.LogWarning("Wrong answer on test {TestNumber}", testCase.Number); await SendStatusAsync(request, State.Done, ErrorCode.IncorrectAnswer, $"Wrong answer on test {testCase.Number}", testCase.Number, package.TestCases.Count); - CleanupWorkingDirectory(package.WorkingDirectory); + CleanupWorkingDirectory(package.ExtractionRoot ?? package.WorkingDirectory); return; } @@ -160,7 +160,7 @@ public class TestingService : ITestingService "All tests passed", package.TestCases.Count, package.TestCases.Count); // Cleanup - CleanupWorkingDirectory(package.WorkingDirectory); + CleanupWorkingDirectory(package.ExtractionRoot ?? package.WorkingDirectory); } catch (Exception ex) { diff --git a/src/LiquidCode.Tester.Worker/appsettings.json b/src/LiquidCode.Tester.Worker/appsettings.json index 7ec48f4..0f9a340 100644 --- a/src/LiquidCode.Tester.Worker/appsettings.json +++ b/src/LiquidCode.Tester.Worker/appsettings.json @@ -6,6 +6,9 @@ } }, "AllowedHosts": "*", + "Isolate": { + "MaxBoxes": 100 + }, "Cpp": { "Compiler": "g++", "CompilerFlags": "-O2 -std=c++17 -Wall", @@ -72,6 +75,7 @@ }, "Python": { "Executable": "python3", + "ValidateSyntax": true, "Versions": { "3.8": { "Executable": "python3.8" diff --git a/tests/LiquidCode.Tester.Worker.Tests/PackageParserServiceTests.cs b/tests/LiquidCode.Tester.Worker.Tests/PackageParserServiceTests.cs index 903ccad..86288fe 100644 --- a/tests/LiquidCode.Tester.Worker.Tests/PackageParserServiceTests.cs +++ b/tests/LiquidCode.Tester.Worker.Tests/PackageParserServiceTests.cs @@ -147,10 +147,10 @@ public class PackageParserServiceTests : IDisposable // 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); + // Assert + Assert.Equal(2, result.TestCases.Count); // Only tests with complete I/O pairs + Assert.Equal(1, result.TestCases[0].Number); + Assert.Equal(2, result.TestCases[1].Number); } [Fact] @@ -171,9 +171,9 @@ public class PackageParserServiceTests : IDisposable 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); + Assert.Equal(2, result.TestCases.Count); // Only tests with complete I/O pairs + Assert.Equal(1, result.TestCases[0].Number); + Assert.Equal(2, result.TestCases[1].Number); } [Fact] @@ -206,9 +206,11 @@ public class PackageParserServiceTests : IDisposable 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); + using (var entryStream = entry.Open()) + using (var writer = new StreamWriter(entryStream)) + { + writer.Write(content); + } } } diff --git a/tests/LiquidCode.Tester.Worker.Tests/PolygonPackageIntegrationTests.cs b/tests/LiquidCode.Tester.Worker.Tests/PolygonPackageIntegrationTests.cs new file mode 100644 index 0000000..139c5d5 --- /dev/null +++ b/tests/LiquidCode.Tester.Worker.Tests/PolygonPackageIntegrationTests.cs @@ -0,0 +1,152 @@ +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using LiquidCode.Tester.Worker.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace LiquidCode.Tester.Worker.Tests; + +public class PolygonPackageIntegrationTests +{ + private readonly PackageParserService _parserService; + + public PolygonPackageIntegrationTests() + { + var configuration = new ConfigurationBuilder().Build(); + + var cppCompilation = new CppCompilationService(NullLogger.Instance, configuration); + var cppExecution = new CppExecutionService(NullLogger.Instance); + + var services = new ServiceCollection(); + services.AddSingleton(configuration); + services.AddSingleton(cppCompilation); + services.AddSingleton(cppExecution); + + var serviceProvider = services.BuildServiceProvider(); + + var compilationFactory = new CompilationServiceFactory(serviceProvider, NullLogger.Instance); + var executionFactory = new ExecutionServiceFactory(serviceProvider, NullLogger.Instance); + var answerGenerator = new AnswerGenerationService(compilationFactory, executionFactory, NullLogger.Instance); + var polygonParser = new PolygonProblemXmlParser(NullLogger.Instance); + + _parserService = new PackageParserService( + NullLogger.Instance, + polygonParser, + answerGenerator, + cppCompilation); + } + + [Fact] + public async Task ParsePolygonPackageAsync_GeneratesTestsCheckerAndAnswers() + { + if (!IsExecutableAvailable("g++")) + { + return; + } + + var packageDirectory = ResolvePackageDirectory("exam-queue-17"); + Assert.True(Directory.Exists(packageDirectory)); + + await using var packageStream = CreateZipFromDirectory(packageDirectory); + + var package = await _parserService.ParsePackageAsync(packageStream); + + try + { + Assert.Equal(51, package.TestCases.Count); + Assert.NotNull(package.CheckerPath); + Assert.True(File.Exists(package.CheckerPath), $"Checker not present at {package.CheckerPath}"); + + foreach (var testCase in package.TestCases) + { + Assert.True(File.Exists(testCase.InputFilePath), $"Missing input {testCase.InputFilePath}"); + Assert.True(File.Exists(testCase.OutputFilePath), $"Missing output {testCase.OutputFilePath}"); + Assert.True(new FileInfo(testCase.InputFilePath).Length > 0); + Assert.True(new FileInfo(testCase.OutputFilePath).Length > 0); + } + } + finally + { + if (!string.IsNullOrEmpty(package.ExtractionRoot) && Directory.Exists(package.ExtractionRoot)) + { + Directory.Delete(package.ExtractionRoot, recursive: true); + } + else if (Directory.Exists(package.WorkingDirectory)) + { + Directory.Delete(package.WorkingDirectory, recursive: true); + } + } + } + + private static bool IsExecutableAvailable(string executable) + { + try + { + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = executable, + Arguments = "--version", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + process.Start(); + if (!process.WaitForExit(5000)) + { + try + { + process.Kill(entireProcessTree: true); + } + catch + { + // ignored + } + + return false; + } + + return process.ExitCode == 0; + } + catch + { + return false; + } + } + + private static MemoryStream CreateZipFromDirectory(string directoryPath) + { + var memoryStream = new MemoryStream(); + using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, leaveOpen: true)) + { + foreach (var file in Directory.GetFiles(directoryPath, "*", SearchOption.AllDirectories)) + { + var entryName = Path.GetRelativePath(directoryPath, file).Replace("\\", "/"); + var entry = archive.CreateEntry(entryName, CompressionLevel.Fastest); + + using (var entryStream = entry.Open()) + using (var fileStream = File.OpenRead(file)) + { + fileStream.CopyTo(entryStream); + } + } + } + + memoryStream.Position = 0; + return memoryStream; + } + + private static string ResolvePackageDirectory(string folderName) + { + var baseDirectory = AppContext.BaseDirectory; + var root = Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "..", "..")); + return Path.Combine(root, folderName); + } +} diff --git a/tests/LiquidCode.Tester.Worker.Tests/PolygonPackageParserTests.cs b/tests/LiquidCode.Tester.Worker.Tests/PolygonPackageParserTests.cs index f29574c..fbd78d1 100644 --- a/tests/LiquidCode.Tester.Worker.Tests/PolygonPackageParserTests.cs +++ b/tests/LiquidCode.Tester.Worker.Tests/PolygonPackageParserTests.cs @@ -166,17 +166,21 @@ public class PolygonPackageParserTests : IDisposable { // Add problem.xml var xmlEntry = archive.CreateEntry("problem.xml"); - using var xmlStream = xmlEntry.Open(); - using var xmlWriter = new StreamWriter(xmlStream); - xmlWriter.Write(problemXml); + 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); + using (var entryStream = entry.Open()) + using (var writer = new StreamWriter(entryStream)) + { + writer.Write(content); + } } } @@ -193,9 +197,11 @@ public class PolygonPackageParserTests : IDisposable 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); + using (var entryStream = entry.Open()) + using (var writer = new StreamWriter(entryStream)) + { + writer.Write(content); + } } } diff --git a/tests/LiquidCode.Tester.Worker.Tests/TestingServiceTests.cs b/tests/LiquidCode.Tester.Worker.Tests/TestingServiceTests.cs index c425e62..e9025dc 100644 --- a/tests/LiquidCode.Tester.Worker.Tests/TestingServiceTests.cs +++ b/tests/LiquidCode.Tester.Worker.Tests/TestingServiceTests.cs @@ -61,6 +61,7 @@ public class TestingServiceTests : IDisposable var emptyPackage = new ProblemPackage { WorkingDirectory = _testDirectory, + ExtractionRoot = _testDirectory, TestCases = new List() // Empty list! }; @@ -103,6 +104,7 @@ public class TestingServiceTests : IDisposable await File.WriteAllTextAsync(inputFile, "test input"); await File.WriteAllTextAsync(outputFile, "expected output"); await File.WriteAllTextAsync(executablePath, "dummy"); + await CreateEmptyPackage(packageFilePath); var request = new TestRequest { @@ -118,6 +120,7 @@ public class TestingServiceTests : IDisposable var package = new ProblemPackage { WorkingDirectory = _testDirectory, + ExtractionRoot = _testDirectory, TestCases = new List { new TestCase @@ -167,7 +170,11 @@ public class TestingServiceTests : IDisposable }); _outputCheckerMock - .Setup(x => x.CheckOutputAsync(It.IsAny(), It.IsAny())) + .Setup(x => x.CheckOutputWithCheckerAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) .ReturnsAsync(true); // Act