diff --git a/README.md b/README.md index f45e47e..5a5f852 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,36 @@ Venue автоматически учитывает браузеры игрок При ошибке предыдущий снимок и время остаются видимыми, доступен повтор. «Нет связи» означает отсутствие heartbeat сайта, а не диагностику Wi-Fi. +Блок «Wi-Fi площадки» (этапы 14–15) показывает текущие Wi-Fi-клиенты +всего Cudy: MAC, IP, диапазон и доступный сигнал в dBm. Он не связывает MAC +с UUID браузера или выбранной игрой, не показывает имена клиентов и трафик. +Сборщик читает роутер каждые 10 секунд; при ошибке оставляет последний успешный +снимок и его время, в том числе после перезапуска сборщика. Снимок старше 30 секунд помечается устаревшим; отсутствие +сигнала отображается как «Нет данных». История не сохраняется. + +Поддержан и проверен TR3000 V1 с прошивкой 2.5.27-20260804-183625, штатная +английская HTML-страница клиентов. Неизвестная разметка возвращает ошибку, +а не пустой список. После обновления прошивки нужно проверить адаптер снова. +Настройки Cudy сборщик не меняет; использует только вход и чтение списка. +Он работает отдельным процессом в сети `entrance`, без доступа к БД/RustFS; +backend читает только общий том снимка, подключённый к нему read-only. + +Сбор Cudy необязателен. Для включения создайте `venue/cudy/` с правами 0700, +сохраните пароль администратора в `venue/cudy/password` с правами 0600 и добавьте +в локальный `venue/.env`: + +```dotenv +CUDY_PASSWORD_FILE=/run/secrets/cudy/password +CUDY_URL=http://192.168.10.1 +``` + +Пароль не записывается в `.env`, API, логи или Git; `venue/cudy/` исключён из Git. +URL допускает только числовой локальный IP без credentials и пути. После +обновления артефактов запустите штатный `make venue`; отдельно сборщик backend +бинарника запускается с `--collect-cudy`. Без `CUDY_PASSWORD_FILE` обращения +к роутеру отсутствуют и панель сообщает, что сбор не настроен. Работающая +площадка при реализации этапов 14–15 не обновлялась. + Разработчик без роли организатора/администратора/автора в venue получает список игр и только вкладку состояния: сведения игры ограничены ID, названием, описанием, форматом, местом, плановым началом и статусом. Команды, пароли, diff --git a/docs/agent/TASKS.md b/docs/agent/TASKS.md index 1c8e494..671163c 100644 --- a/docs/agent/TASKS.md +++ b/docs/agent/TASKS.md @@ -27,13 +27,13 @@ ### ED-029 — Автономная площадка и панель организатора -- Статус: `IN_PROGRESS` — этап 3 принят пользователем без WAN; этапы 4–13 завершены; аппаратная приёмка этапа 8 подтверждена пользователем. +- Статус: `IN_PROGRESS` — этап 3 принят пользователем без WAN; этапы 4–14 завершены; автоматическая часть этапа 15 завершена, аппаратная приёмка остаётся; этап 8 принят пользователем. - Репозитории: корневой, `evening_detective_server`, `evening_detective_frontend`. - Цель: реализовать принятый пользователем план из 15 последовательных этапов: автономное окружение; локальные организаторы; прежние адреса через Cudy; пакет игры; экспорт со статусом офлайн-режима; локальный импорт; интерфейс передачи; полный билетный и игровой flow; пакет результатов; возврат онлайн; интерфейс возврата; устройства; панель состояния; сбор данных Cudy; Wi-Fi и испытание площадки. - Критерии: проверяемый результат каждого этапа; цикл «онлайн → ноутбук → проведение → онлайн» через интерфейс с сохранением QR, билетов и связей; повторные импорты без дублей; мониторинг устройств и Wi-Fi; полный прогон без WAN, 100 клиентов и проверка покрытия. - Ограничения: только тестовые игры до завершения возврата; существующие правила игры и формат `offline` сохраняются; без несогласованных новых зависимостей и отклонений от стандартов; без персональных списков игроков, CPU/памяти Cudy и автоматического переключения серверов; настройки роутера менять только по явному запросу пользователя; секреты и пакеты вне Git; commit/push выполняет пользователь. - Проверка: Go-стандарт и `go test ./...`; PostgreSQL-интеграционные тесты для атомарности; frontend-стандарт, type-check, lint, lint:css и build; штатная генерация protobuf; ручные контрольные точки после этапов 3, 8, 11, 15. -- Прогресс: этапы 1–2 реализованы и проверены в локальном Docker-окружении. Добавлены отдельный Compose, подготовка образов заранее, постоянные тома, миграции перед стартом, healthchecks и прокси. Явный `APP_MODE=venue` отключает SMTP, фоновые письма и внешний webhook; почтовые операции аккаунта возвращают безопасную ошибку до изменения данных. Локальные аккаунты создаются штатным `dev-user.sql`, роли и правила автоматического подтверждения команд сохраняются. Этап 3 принят пользователем после проверки без WAN; формат и код пакета игры (этап 4) реализованы и проверены. Экспорт со статусом офлайн-режима (этап 5) и локальный импорт (этап 6) реализованы и проверены. Этап 7 реализован и проверен; этап 8 проверен на изолированном стенде и принят пользователем на аппаратной контрольной точке; этапы 9–13 реализованы и проверены; этапы 14–15 ещё не завершены. +- Прогресс: этапы 1–2 реализованы и проверены в локальном Docker-окружении. Добавлены отдельный Compose, подготовка образов заранее, постоянные тома, миграции перед стартом, healthchecks и прокси. Явный `APP_MODE=venue` отключает SMTP, фоновые письма и внешний webhook; почтовые операции аккаунта возвращают безопасную ошибку до изменения данных. Локальные аккаунты создаются штатным `dev-user.sql`, роли и правила автоматического подтверждения команд сохраняются. Этап 3 принят пользователем после проверки без WAN; формат и код пакета игры (этап 4) реализованы и проверены. Экспорт со статусом офлайн-режима (этап 5) и локальный импорт (этап 6) реализованы и проверены. Этап 7 реализован и проверен; этап 8 проверен на изолированном стенде и принят пользователем на аппаратной контрольной точке; этапы 9–13 реализованы и проверены; этап 14 реализован и проверен на настоящем TR3000; код и автоматические проверки этапа 15 завершены, ожидается аппаратная приёмка Wi-Fi. - Дополнение по запросу пользователя: структура `venue` приведена к `prod` (`backend/`, `frontend/dist/`, `migrations/`, `Dockerfile.backend`, `docker-compose.yml`, `deploy.sh`); имена основных Compose-сервисов совпадают. Общая генерация `make build` / `make prod-build` готовит артефакты обеих папок; frontend собирается отдельно с настройками своего окружения. Текущие файлы `prod` при этой работе не изменяются; сборка проверяется с production-выводом в `/tmp`. - Упрощение запуска: отдельные `init.sh` и `prepare.sh` удалены, всё собрано в `deploy.sh`. Единственная команда запуска — `make venue`: создаёт `.env` только при его отсутствии, при необходимости готовит зависимости/образы и автоматически создаёт организатора. Повторный готовый запуск не собирает и не скачивает образы. `make venue-update` явно пересобирает окружение с интернетом. Аккаунт организатора синхронизируется со значениями `.env` при каждом запуске. Первый запуск, повторный запуск без интернета и обновление проверяются `python3 tests/venue/deploy.py` с подставными командами; реальный готовый запуск проверен через Docker и API-smoke. - Настройки площадки: по запросу пользователя необязательный `venue/.env.example` удалён; настройки создаются автоматически через `make venue`. Убраны ссылка и проверка примера; `python3 tests/venue/deploy.py` прошёл. Существующая `.env` не изменялась. @@ -84,7 +84,10 @@ - Результат этапа 13 (2026-10-11): venue-вкладка «Состояние» `/games/:id?tab=state` показывает серверные connected/serverTime, количество доступных устройств/устройств без связи/подключений, UUID профиля, последние обращения и раскрываемые подключения с типом, ID команды/экрана, статусом, IP и User-Agent. Нет связи идёт первым, затем UUID; непривязанные браузеры исключает фильтр игры. Generated GetDevices вызывается сразу и каждые 5 секунд через useSimplePolling только на активной вкладке, без параллельных запросов; поздние ответы после смены игры, ухода и уничтожения игнорируются. Первичная загрузка, пустой список, встроенная ошибка, сохранение предыдущего снимка/времени и повтор реализованы без повторных toast. Вкладка доступна с клавиатуры. Игровые показатели, Cudy и история присутствия не добавлялись. - Доступ этапа 13: GetGames/GetGame и UI permissions расширены для разработчика только в venue. Разработчик без организатора/администратора/автора получает явный whitelist ID/названия/описания/формата/места/планового начала/статуса; GetGame использует краткое чтение без сценария/команд. UI скрывает создание и результаты, показывает название и только состояние, нормализует другие tab, не загружает экраны/музыку/план/карту/ответы; heartbeat остаётся браузерным. Дополнительные роли, импорт и запреты игровых действий сохранены; prod новые права не получает. При смене роли полный поздний GetGame не применяется, приватные баннер/модалки скрываются. - Проверки этапа 13: gofmt, полный go test ./..., race-тесты device_presence_service/app с управляемыми часами этапа 12; frontend type-check/lint/lint:css/build и self-check gameState прошли. Self-check покрывает сводку/сортировку, активность, отсутствие параллельных запросов, поздние ответы, ошибку/восстановление. Изолированные PostgreSQL/RustFS API-регрессии и Firefox проверили организатора, три подключения одного профиля, клавиатуру, смену контекста, пустой список, потерю/восстановление GetDevices, остановку запросов на другой вкладке, разработчика venue/prod, безопасные GetGame/GetGames и отсутствие закрытых фоновых запросов. API/protobuf/миграции/зависимости не менялись. Ручное ревью diff по Go/frontend-стандартам и независимое read-only ревью выполнены; пользовательские изменения prod/reproxy/backend-бинарника сохранены. Работающие окружения и Cudy не менялись. -- Следующий шаг: этап 14 — сбор данных Cudy. Этап 13 завершён; общая задача остаётся `IN_PROGRESS` и не переносится в архив. ZIP остаётся неподписанным. Фактический ZIP 1 ГиБ, нагрузка на 100 клиентов и покрытие Wi-Fi не проверялись; развёртывание, commit/push не выполнялись. +- Результат этапа 14 (2026-10-11): отдельный процесс того же backend-бинарника `--collect-cudy` читает текущих Wi-Fi-клиентов Cudy через штатный вход и GET страницы клиентов раз в 10 секунд. Проверен настоящий TR3000 V1 2.5.27-20260804-183625; настройки не менялись. Собираются только MAC/IP/диапазон/доступный сигнал dBm, без имён, трафика и истории. Пароль читается из локального файла, не выдаётся API/логами; `venue/cudy/` исключён из Git. URL ограничен числовым private/loopback IP, redirects тем же host/scheme, ответ 2 МиБ, HTTP timeout 5 секунд, сессия переиспользуется и восстанавливается. Неизвестная разметка вызывает ошибку вместо ложного пустого списка. Collector изолирован в entrance без БД/RustFS; backend сохраняет внутреннюю сеть и читает общий атомарный снимок read-only. Без credential-файла обращений к роутеру нет. +- Результат автоматической части этапа 15: Wi-Fi-блок вкладки состояния показывает весь Cudy независимо от игры, MAC/IP/диапазон/dBm или «Нет данных», число клиентов, последний успешный снимок, ошибку/устаревание. При ошибке collector сохраняет прежние данные и время, включая перезапуск; свежесть 30 секунд считает сервер, история не сохраняется. MAC не связывается предположениями с UUID браузера. Существующий GetDevices расширен Wi-Fi-полем с теми же venue/role guards, нового endpoint/миграций/зависимостей нет. Обновлены protobuf/OpenAPI и оба generated clients. +- Проверки этапов 14–15: gofmt, `go test ./...`, race Cudy/wifi/presence/app, frontend type-check/lint/lint:css/build и self-check состояния прошли. Mock Cudy проверяет hash/CSRF/session/readonly GET, DOM/оба диапазона/сигнал/неизвестную разметку; снимок проверен атомарно и с управляемыми часами; CLI self-check подтвердил сохранение клиентов/времени после перезапуска при недоступном роутере. Firefox выявил прежнюю гонку раннего возврата ZIP до загрузки ID игры; действия передачи/возврата теперь появляются только после загрузки положительного ID. Итоговый изолированный Firefox-прогон обоих форматов (1440/390 px) прошёл: Wi-Fi fixtures, устройства/роли/polling и полный цикл ZIP до повторного возврата после перезапуска. Полный API-flow с PostgreSQL/race-регрессиями также прошёл; временные стенды удалены. `load.py` прошёл на отдельном Docker-стенде без WAN: 100 виртуальных профилей, 25 команд, 60 секунд, 300 heartbeat, 1200 story, 50 ходов, 25 ответов и 12 опросов состояния; ровно 100 доступных устройств и ноль HTTP-ошибок. p95: heartbeat 118 мс, story 68 мс, move 109 мс, answers 45 мс, devices 138 мс (бюджет теста 2 с). Нагрузка backend идёт по HTTP напрямую; DNS/HTTPS/Nginx проверены отдельно `dns.py --docker`/`https.py --docker`. Ручное ревью по стандартам и независимое read-only ревью выполнены; замечание о ложном пустом DOM исправлено и покрыто регрессией. +- Следующий шаг: аппаратная контрольная точка этапа 15 по `tests/venue/README.md`: после отдельно согласованного обновления площадки Android/iPhone, отключённые WAN/мобильные данные, 100 настоящих Wi-Fi-устройств и покрытие зала. Пользователь выбрал автоматическую нагрузку 100 виртуальных клиентов с отдельной ручной аппаратной приёмкой. До её подтверждения ED-029 остаётся `IN_PROGRESS` и не переносится в архив. Всего 15 этапов. ZIP остаётся неподписанным, фактический ZIP 1 ГиБ не проверен; развёртывание, commit/push не выполнялись. ### ED-024 — Общая дизайн-система фронтенда diff --git a/tests/venue/README.md b/tests/venue/README.md index 2d73f1d..375ea31 100644 --- a/tests/venue/README.md +++ b/tests/venue/README.md @@ -62,7 +62,8 @@ python3 tests/venue/flow.py --apply --browser (`offline`). Проверяются клавиатурный focus, отказ на ход с сохранением ввода, блокировка повторной отправки и кнопки на паузе при уже введённом коде. Локальный HTTP-прокси переключает один публичный адрес между двумя реальными -backend, имитируя смену DNS; API-ответы не подменяются. Тестовые ссылки, ZIP, +backend, имитируя смену DNS; Игровые API-ответы не подменяются; ошибки, пустой список и состояния Wi-Fi +панели мониторинга дополнительно проверяются управляемыми ответами GetDevices. Тестовые ссылки, ZIP, профиль Firefox и сборки остаются во временном каталоге до конца прогона. `--results` дополнительно проверяет этап 9: серверный сервис собирает ZIP @@ -155,3 +156,52 @@ Firefox проверяет игрока, организатора и экран ошибку/восстановление и отбрасывание ответов после смены игры, ухода и unmount. Временные границы connected остаются в тестах сервиса этапа 12 с управляемыми часами; клиент пороги не пересчитывает. + + +## Wi-Fi и нагрузка (этапы 14–15) + +```sh +python3 tests/venue/load.py +python3 tests/venue/flow.py --apply --browser +``` + +`load.py` использует тот же одноразовый стенд без WAN: 100 виртуальных UUID, +25 новых команд по четыре браузера, 60 секунд. Каждый профиль отправляет +heartbeat раз в 20 секунд и читает историю раз в 5 секунд; по одному профилю +команды выполняют два хода и сохраняют ответ. Организатор опрашивает состояние +раз в 5 секунд. Проверяются ровно 100 доступных устройств, 50 сохранённых ходов, +25 ответивших команд и отсутствие HTTP-ошибок. Отчёт содержит число запросов, +p50/p95/max; p95 ≤ 2 секунды — бюджет этого теста, не обещание для любого железа. +Запросы идут прямо к backend по HTTP: DNS, HTTPS и прокси проверяются отдельно +`dns.py --docker` и `https.py --docker`. Это нагрузка backend, не проверка радио. + +Go-тесты адаптера Cudy проверяют вход/продление сессии, ограничение URL/ответа, +разбор обеих полос, отсутствующий сигнал, ошибку при неизвестной разметке, +атомарный снимок, свежесть с управляемыми часами и venue/prod/роли. Firefox +проверяет MAC/IP/диапазон/dBm, устаревание, ошибку и отсутствие сигнала; эти +состояния Wi-Fi задаются fixture. Отдельно адаптер проверен чтением настоящего +TR3000 V1 2.5.27-20260804-183625 без изменения настроек роутера. + +### Аппаратная контрольная точка этапа 15 + +После согласованного обновления реальной площадки, только на тестовой игре: + +1. Включить сбор Cudy по корневому README; отключить WAN и мобильные данные. + На Android/iPhone открыть прежние домены, проверить DNS и доверенный HTTPS. +2. Подключить 100 настоящих Wi-Fi-устройств (несколько вкладок одного профиля + не заменяют устройства); открыть игру 25 командами, держать нагрузку + минимум 60 секунд, выполнять ходы и сохранять ответы. +3. Сверить клиентов Cudy с панелью, диапазоны и доступный сигнал; пройти края + и проблемные зоны площадки, проверить материалы и действия в каждой зоне. + MAC/UUID не сопоставлять предположениями; «Нет связи» относится к heartbeat. +4. Проверить паузу/продолжение, перезапуск venue и повторную регистрацию + браузеров, смену контекста/несколько вкладок; убедиться, что панели в prod нет. +5. Временно остановить сборщик: через 30 секунд снимок должен устареть, + прежние данные и время остаться видимыми. Запустить снова и проверить + восстановление. Потерю API и восстановление проверить без потери снимка. +6. Завершить игру, вернуть ZIP на источник, проверить результаты и прежние + ссылки. Зафиксировать оборудование, число клиентов, длительность, + задержки/ошибки и покрытие; сообщить результат для приёмки ED-029. + +Автоматические проверки не заменяют эту приёмку. Пока она не подтверждена, +ED-029 остаётся `IN_PROGRESS`. Фактический ZIP 1 ГиБ также не проверен. diff --git a/tests/venue/flow_browser.py b/tests/venue/flow_browser.py index b55c0f2..a68c9c3 100644 --- a/tests/venue/flow_browser.py +++ b/tests/venue/flow_browser.py @@ -76,7 +76,7 @@ def check_browser_flow(stand, fixtures): if js('return ' + script): return time.sleep(.1) - raise AssertionError('Browser timed out: ' + description) + raise AssertionError('Browser timed out: ' + description + ': ' + str(js('return document.body.innerText')).replace('\n', ' ')[:1500]) def navigate(path): command('WebDriver:Navigate', {'url': stand.public + path}) @@ -177,6 +177,10 @@ def check_browser_flow(stand, fixtures): wait("!document.body.innerText.includes('Не удалось обновить состояние')", 'state recovery') js("window.stateMode='empty';return true;") wait("document.body.innerText.includes('Устройства этой игры пока не зарегистрированы')", 'empty devices', 10) + js("window.fetch=(url,options)=>{if(String(url).includes('/api/devices?')){window.stateCalls++;return Promise.resolve(new Response(JSON.stringify({devices:[],serverTime:new Date().toISOString(),wifi:{enabled:true,hasError:window.wifiFailed,stale:window.wifiFailed,collectedAt:new Date().toISOString(),clients:[{MAC:'AA:BB:CC:DD:EE:01',IP:'192.168.10.9',band:'5 GHz',signal:window.wifiFailed?undefined:-65}]}}),{headers:{'Content-Type':'application/json'}}));}return window.stateFetch(url,options)};return true;") + wait("document.body.innerText.includes('-65 dBm')", 'Wi-Fi client and dBm', 10) + js("window.wifiFailed=true;return true;") + wait("document.body.innerText.includes('Данные устарели') && document.body.innerText.includes('Не удалось обновить данные Cudy') && document.body.innerText.includes('Нет данных')", 'stale Wi-Fi and missing signal', 10) assert js("const tab=[...document.querySelectorAll('.n-tabs-tab[data-name]')].find(t=>t.textContent.trim()==='Игра');tab.click();return true;") wait("location.search.includes('tab=game')", 'leave state tab') calls = js('return window.stateCalls') diff --git a/tests/venue/https.py b/tests/venue/https.py index a7a617f..532931a 100644 --- a/tests/venue/https.py +++ b/tests/venue/https.py @@ -181,6 +181,15 @@ with tempfile.TemporaryDirectory(prefix="venue-https-") as directory: assert {(p["protocol"], str(p["published"])) for p in dns["ports"]} == {("udp", "53"), ("tcp", "53")} assert all(p["host_ip"] == "127.0.0.1" for p in dns["ports"]) assert any(v["target"] == "/etc/coredns/Corefile" and v["read_only"] for v in dns["volumes"]) + collector = https["services"]["cudy_collector"] + backend = https["services"]["evening_detective_server"] + assert set(collector["networks"]) == {"entrance"} and not collector.get("ports") + assert set(backend["networks"]) == {"default"} + assert "DATABASE_URL" not in collector["environment"] and "S3_PASSWORD" not in collector["environment"] + assert any(v["target"] == "/run/secrets/cudy" and v["read_only"] for v in collector["volumes"]) + assert any(v["target"] == "/run/cudy" and v["read_only"] for v in backend["volumes"]) + assert not any(v["target"] == "/run/secrets/cudy" for v in backend["volumes"]) + assert collector["environment"]["CUDY_SNAPSHOT_FILE"] == server["CUDY_SNAPSHOT_FILE"] print("PASS: Compose merge, HTTPS addresses, ports, read-only mounts and preserved data volumes") suffix = uuid.uuid4().hex[:10] network = "venue-https-test-" + suffix diff --git a/tests/venue/load.py b/tests/venue/load.py new file mode 100644 index 0000000..5a7204e --- /dev/null +++ b/tests/venue/load.py @@ -0,0 +1,98 @@ +"""100 virtual clients in disposable prod/venue; does not test Wi-Fi radios.""" +import json +import math +from concurrent.futures import ThreadPoolExecutor +import statistics +import sys +import tempfile +import threading +import time +import uuid + +sys.dont_write_bytecode = True +from flow import API, Stand, export_package, fixture + + +def check_load(stand): + data = fixture(stand, 'online') + venue = stand.apis['venue'] + venue.request('/api/games/import', export_package(stand.apis['prod'], data['game'])) + game = data['game'] + teams = [venue.request('/api/teams', {'gameID': game, 'name': f'TEST load {i}', 'participantsCount': 4}) for i in range(25)] + venue.request(f'/api/games/{game}/start', {}, 'PUT') + samples = {} + lock = threading.Lock() + barrier = threading.Barrier(101) + ids = [str(uuid.uuid4()) for _ in range(100)] + connections = [str(uuid.uuid4()) for _ in range(100)] + + def measured(name, action): + before = time.perf_counter() + result = action() + with lock: + samples.setdefault(name, []).append(time.perf_counter() - before) + return result + + def client(index): + public = API(venue.base) + team = teams[index // 4] + heartbeat = {'deviceID': ids[index], 'connectionID': connections[index], 'teamID': team['ID'], 'password': team['password']} + barrier.wait(timeout=30) + started = time.monotonic() + next_heartbeat = next_story = started + moved = False + while time.monotonic() - started < 60: + now = time.monotonic() + if now >= next_heartbeat: + result = measured('heartbeat', lambda: public.request('/api/devices/heartbeat', heartbeat)) + assert result['enabled'] is True + next_heartbeat += 20 + if not moved and index % 4 == 0: + for code in ('p1', 'p2'): + measured('move', lambda code=code: public.move(team, code)) + measured('answers', lambda: public.request(f'/api/teams/{team["ID"]}/answers', {'password': team['password'], 'answers': [{'questionCode': 'q1', 'answer': 'Butler'}]})) + moved = True + if now >= next_story: + measured('story', lambda: public.story(team)) + next_story += 5 + time.sleep(.05) + + with ThreadPoolExecutor(max_workers=100) as pool: + futures = [pool.submit(client, index) for index in range(100)] + barrier.wait(timeout=30) + deadline = time.monotonic() + 60 + while time.monotonic() < deadline: + snapshot = measured('devices', lambda: venue.request(f'/api/devices?gameID={game}')) + assert len(snapshot['devices']) <= 100 + time.sleep(5) + for future in futures: + future.result() + devices = venue.request(f'/api/devices?gameID={game}')['devices'] + assert {device['ID'] for device in devices} == set(ids) + assert all(device['connected'] and len(device['connections']) == 1 for device in devices) + for team in teams: + assert len(API(venue.base).story(team)['actions']) == 2 + answers = venue.request(f'/api/games/{game}/answers')['teams'] + assert {row['team']['ID'] for row in answers if any(answer.get('answer') == 'Butler' for answer in row.get('answers', []))} == {team['ID'] for team in teams} + report = {} + for name, values in sorted(samples.items()): + values.sort() + p95 = values[math.ceil(.95 * len(values)) - 1] + report[name] = {'requests': len(values), 'p50_ms': round(statistics.median(values) * 1000), 'p95_ms': round(p95 * 1000), 'max_ms': round(values[-1] * 1000)} + assert p95 <= 2, f'{name} exceeds the test budget p95 <= 2s' + print('PASS: 100 profiles, 25 teams, 60s, 100 connected devices, 50 moves, 25 answers, zero HTTP errors', flush=True) + print(json.dumps(report, sort_keys=True), flush=True) + + +def main(): + with tempfile.TemporaryDirectory(prefix='ed029-load-') as directory: + stand = Stand(directory) + try: + stand.start() + check_load(stand) + finally: + stand.close() + + +if __name__ == '__main__': + main() diff --git a/venue/.gitignore b/venue/.gitignore index 0adf1cd..da01636 100644 --- a/venue/.gitignore +++ b/venue/.gitignore @@ -4,3 +4,5 @@ frontend/dist/ migrations/ certificates/ packages/ + +/cudy/ diff --git a/venue/docker-compose.yml b/venue/docker-compose.yml index e6c9418..9c1f3ac 100644 --- a/venue/docker-compose.yml +++ b/venue/docker-compose.yml @@ -5,7 +5,7 @@ x-app: &app build: context: . dockerfile: Dockerfile.backend - environment: + environment: &app-environment APP_MODE: venue DATABASE_URL: postgresql://${POSTGRES_USER:?}:${POSTGRES_PASSWORD:?}@postgres_evening_detective_server:5432/${POSTGRES_DB:?}?sslmode=disable APP_BASE_URL: ${APP_BASE_URL:?} @@ -64,6 +64,11 @@ services: evening_detective_server: <<: *app + environment: + <<: *app-environment + CUDY_SNAPSHOT_FILE: /run/cudy/state.json + volumes: + - cudy_state:/run/cudy:ro depends_on: migrate: condition: service_completed_successfully @@ -102,6 +107,20 @@ services: condition: service_completed_successfully restart: "no" + cudy_collector: + image: evening-detective-venue-app:1 + command: [/usr/local/bin/evening_detective_server, --collect-cudy] + environment: + CUDY_URL: ${CUDY_URL:-http://192.168.10.1} + CUDY_PASSWORD_FILE: ${CUDY_PASSWORD_FILE:-} + CUDY_SNAPSHOT_FILE: /run/cudy/state.json + volumes: + - ./cudy:/run/secrets/cudy:ro + - cudy_state:/run/cudy + networks: + - entrance + restart: unless-stopped + evening_detective_frontend: image: byjg/static-httpserver:latest environment: @@ -129,6 +148,7 @@ services: restart: unless-stopped volumes: + cudy_state: # Preserve existing data from the former Compose project. postgres_data: name: evening-detective-local_postgres_data