diff --git a/.gitignore b/.gitignore index 76b4d63..6bbb112 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ *.so *.dylib .idea +playwright +*.log # Test binary, built with `go test -c` *.test @@ -31,3 +33,11 @@ go.work.sum # Editor/IDE # .idea/ # .vscode/ + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..d3f1dc1 --- /dev/null +++ b/README.md @@ -0,0 +1,367 @@ +# 🐂 Ox — Нагрузочное тестирование 1С + +![logo.png](docs/img/logo.png) + +> Кроссплатформенный инструмент для нагрузочного тестирования веб-клиента 1С на базе Playwright + +## 📋 Содержание + +- [Введение](#введение) +- [Возможности и ограничения](#возможности-и-ограничения) +- [Установка](#установка) +- [Архитектура](#архитектура) +- [Быстрый старт](#быстрый-старт) +- [Создание тестовых сценариев](#создание-тестовых-сценариев) +- [Отладка и диагностика](#отладка-и-диагностика) +- [Документация](#документация) + +## 📖 Введение + +**Ox** — это открытоисходный инструмент для нагрузочного тестирования веб-клиента 1С. Он позволяет эмулировать действия пользователей и создавать нагрузку на тестируемую систему без необходимости глубокого понимания архитектуры Test Center 1C. + +Проект создан на базе [Playwright](https://playwright.dev/) и предоставляет простой и удобный способ записи и воспроизведения пользовательских сценариев с поддержкой множественных параллельных исполнителей. + +### Почему Ox? + +В экосистеме открытого ПО отсутствует достойная альтернатива Test Center 1C для нагрузочного тестирования. Хотя Ox не претендует на полную замену Test Center (который предоставляет больше функций и возможностей), он компенсирует это **простотой использования**: + +- ✅ Интуитивная запись сценариев (как встроенный менеджер тестирования в 1С) +- ✅ Не требует специальной квалификации от инженеров +- ✅ Удобная панель управления и распределённое выполнение тестов +- ✅ Автоматическая запись скриншотов и видео при сбоях + +## 🎯 Возможности и ограничения + +### ✅ Что поддерживает Ox + +- **Простая эксплуатация** — минимальная настройка и кривая обучения +- **Запись сценариев через UI** — как встроенный менеджер тестирования в 1С +- **Скриншоты и видео при ошибках** — автоматическая диагностика сбоев +- **Распределённое выполнение** — запуск множества worker'ов на разных машинах +- **Параллельные потоки** — настраиваемое количество одновременных тестов на worker'е +- **Панель управления** — веб-интерфейс для оркестрации и мониторинга +- **Выявление проблем** — логи worker'ов содержат подробную диагностику ошибок + +### ❌ Ограничения + +- **Только веб-клиент** — не поддерживает толстый клиент 1С +- **Упрощённый функционал** — по сравнению с Test Center 1C +- **Зависит от Playwright** — используются возможности и ограничения браузерной автоматизации + +## 💾 Установка + +### Вариант 1: Из готовых бинарников + +Скачайте последний релиз из [GitHub Releases](https://github.com/LazarenkoA/extensions-info/releases) + +### Вариант 2: Сборка из исходников + +**Требования:** +- [Go](https://go.dev/dl/) 1.21+ +- Make или аналогичный инструмент + +```bash +make build-backend +``` + +**Требования для worker'ов:** +- [Node.js](https://nodejs.org/en/download) 18+ +- Playwright установится автоматически при первом запуске worker'а + +## 🏗️ Архитектура + +Ox состоит из двух компонентов: + +### Worker + +- **Описание:** Рабочий процесс, запускаемый на ВМ, с которой идёт нагрузка +- **Функция:** Выполнение Playwright сценариев в несколько потоков +- **Масштабирование:** Можно запускать множество экземпляров worker'ов +- **Требования:** Node.js, доступ к тестируемому веб-сервису 1С + +### Observer + +- **Описание:** Центральный оркестратор и панель управления +- **Функция:** Управление worker'ами, распределение сценариев, сбор результатов +- **Экземпляры:** Должен быть ровно один экземпляр +- **Требования:** Сетевой доступ ко всем worker'ам по TCP + +## 🚀 Быстрый старт + +### Шаг 1: Запустить Worker + +На машине, с которой будет идти нагрузка: + +```bash +worker.exe -p 55556 +``` + +**Доступные флаги:** +- `-p, --port` — порт для прослушивания (по умолчанию 55556) +- `-h, --host` — адрес для прослушивания (по умолчанию 0.0.0.0) + +Можно запустить несколько экземпляров на разных портах: + +```bash +worker.exe -p 55556 & +worker.exe -p 55557 & +worker.exe -p 55558 & +``` + +### Шаг 2: Настроить Observer + +Отредактируйте конфиг [observer/config.yaml](observer/config.yaml): + +```yaml +workers: + - host: 192.168.1.10 + port: 55556 + - host: 192.168.1.11 + port: 55556 + - host: 192.168.1.11 + port: 55557 + +observer: + port: 8091 + listen: 0.0.0.0 +``` + +### Шаг 3: Запустить Observer + +```bash +observer.exe -c config.yaml +``` + +Observer запустит веб-сервер на `http://localhost:8091` + +### Шаг 4: Проверить статус Worker'ов + +Откройте в браузере `http://localhost:8091` и проверьте, что все worker'ы в статусе **READY**: + +![browser_z2xVfQLybR.png](docs/img/browser_z2xVfQLybR.png) + +#### Устранение проблем с подключением + +**Если worker'ы в статусе OFFLINE:** + +1. Проверьте сетевую доступность: + ```bash + telnet + # или + nc -zv + ``` + +2. Проверьте, что worker'ы запущены и слушают нужные порты: + ```bash + lsof -i : # macOS/Linux + netstat -ano | findstr : # Windows + ``` + +3. Проверьте firewall правила на машинах с worker'ами + +4. Убедитесь, что в конфиге Observer правильно указаны IP-адреса и порты + +## 📝 Создание тестовых сценариев + +### Запись сценария с помощью Codegen + +Используйте встроенный UI редактор Playwright: + +```bash +npx playwright codegen http://localhost/bsp/ru_RU/ +``` + +Откроется два окна: +- **Браузер** — для выполнения действий +- **Inspector** — для просмотра генерируемого кода + +Выполняйте действия в браузере, они автоматически записываются как код. + +### Пример сгенерированного сценария + +```javascript +import { test, expect } from '@playwright/test'; + +test('create_survey_template', async ({ page }) => { + // Переход на систему и авторизация + await page.goto('http://localhost/bsp/ru_RU/'); + await page.locator('#userName').fill('Администратор'); + await page.locator('#userName').press('Tab'); + await page.locator('#userPassword').fill('123'); + await page.locator('#userPassword').press('Enter'); + + // Ожидание загрузки интерфейса + await page.waitForLoadState('networkidle'); + + // Навигация по меню + await page.getByText('Анкетирование').click(); + await page.getByText('Шаблоны анкет').click(); + + // Создание нового шаблона + await page.locator('[id="form4_ФормаСоздать"]').click(); + + const templateName = randomString(); + await page.getByRole('textbox', { name: 'Наименование:' }) + .pressSequentially(templateName); + + await page.locator('[id="form5_Заголовок"] > .inputs') + .pressSequentially(randomString()); + + // Сохранение + await page.locator('[id="form5_ФормаЗаписатьИЗакрыть"]').click(); + + // Удаление созданного шаблона + await page.locator('[id="grid_form4_Список"]') + .getByText(templateName, { exact: true }) + .click({ button: 'right' }); + + await page.locator('#popupItem4') + .getByText('Пометить на удаление / Снять пометку') + .click(); + + await page.locator('#form6_Button0 a') + .filter({ hasText: 'Да' }) + .click(); +}); + +function randomString() { + return Math.random().toString(36).substring(2, 10); +} +``` + +### Рекомендации при редактировании сценариев + +1. **Используйте `.pressSequentially()` вместо `.fill()`** — для элементов с автодополнением и валидацией +2. **Добавляйте явные ожидания** — `await page.waitForLoadState('networkidle')` +3. **Используйте подробные селекторы** — избегайте селекторов, зависящих от порядка элементов +4. **Генерируйте уникальные данные** — используйте функции для создания случайных значений +5. **Обрабатывайте ошибки** — добавляйте проверки важных состояний + +Полная документация: [Playwright Documentation](https://playwright.dev/docs/intro) + +## 🧪 Проверка и отладка сценариев + +### Локальное тестирование в UI режиме + +После записи сценария проверьте его работу: + +```bash +cd playwright +npx playwright test ./tests/bsp.spec.js --project=chromium --ui +``` + +Откроется интерактивное окно, где можно: +- Запустить сценарий целиком или отдельные шаги +- Увидеть скриншот каждого шага +- Отследить выполнение с замедлением + +### Отладка в режиме Inspector'а + +```bash +PWDEBUG=1 npx playwright test ./tests/bsp.spec.js --project=chromium +``` + +Откроется Inspector для пошагового выполнения. + +### Просмотр отчётов + +После выполнения тестов на worker'е, результаты находятся в: + +``` +/playwright/playwright-report/index.html +``` + +Отчёт содержит: +- 📸 Скриншоты каждого шага +- 🎥 Видео выполнения (особенно полезно при ошибках) +- 📋 Список всех действий и их результатов +- ⏱️ Время выполнения каждого шага + +### Распространённые проблемы + +| Проблема | Решение | +|----------|---------| +| Элемент не найден | Проверьте селектор в DevTools браузера, используйте более специфичные селекторы | +| Timeout при ожидании | Увеличьте timeout: `await page.locator(...).click({ timeout: 30000 })` | +| Случайные падения | Добавьте явные ожидания загрузки: `await page.waitForLoadState('networkidle')` | +| Элемент скрыт за другим | Используйте scroll: `await page.locator(...).scrollIntoViewIfNeeded()` | +| Данные не совпадают при повторном запуске | Используйте генерацию уникальных данных для каждого прогона | + +## 📊 Управление тестами из Observer'а + +В веб-интерфейсе Observer'а вы можете: + +1. **Выбрать worker** — на котором запустится тест +2. **Установить параллелизм** — количество одновременных потоков +3. **Загрузить сценарий** — выбрать подготовленный скрипт Playwright +4. **Запустить тест** — начать выполнение +5. **Мониторить статус** — просматривать результаты в реальном времени + +### Интерпретация статусов + +- **READY** — worker подключен и готов к работе +- **RUNNING** — выполняется тест +- **ERROR** — тест завершился с ошибкой (см. логи) +- **OFFLINE** — worker недоступен (проверьте сетевое соединение) + +## 📚 Документация + +- [Playwright Documentation](https://playwright.dev/) +- [1C Web Client Documentation](https://its.1c.eu/db/v8314doc) +- [Структура проекта](docs/ARCHITECTURE.md) +- [Примеры сценариев](examples/) + +## 🐛 Диагностика проблем + +### Логи Worker'а + +При ошибке в тесте: + +```bash +tail -f /logs/worker.log +``` + +### Логи Observer'а + +```bash +tail -f /logs/observer.log +``` + +### Видео и скриншоты при сбое + +Находятся в: +``` +/playwright/playwright-report/index.html +``` + +Видео особенно полезно для понимания того, что произошло перед ошибкой. + +## 🤝 Контрибьютинг + +Приветствуются pull requests с: +- Исправлениями ошибок +- Новыми возможностями +- Улучшениями документации +- Примерами сценариев + +## 📄 Лицензия + +Проект распространяется под лицензией [укажите лицензию] + +## ❓ Часто задаваемые вопросы + +**Q: Можно ли использовать Ox для толстого клиента 1С?** +A: Нет, Ox поддерживает только веб-клиент. + +**Q: Сколько worker'ов можно запустить?** +A: Неограниченно, но рекомендуется учитывать ресурсы машин и лимиты браузеров. + +**Q: Как увеличить количество одновременных тестов?** +A: Используйте слайдер в Observer'е для настройки параллелизма на каждом worker'е. + +**Q: Нужна ли лицензия на Playwright?** +A: Нет, Playwright распространяется бесплатно и с открытыми исходниками. + +## 📞 Поддержка + +Для сообщений об ошибках и вопросов используйте [GitHub Issues](https://github.com/LazarenkoA/ox/issues) \ No newline at end of file diff --git a/docs/img/browser_z2xVfQLybR.png b/docs/img/browser_z2xVfQLybR.png new file mode 100644 index 0000000..daeef76 Binary files /dev/null and b/docs/img/browser_z2xVfQLybR.png differ diff --git a/docs/img/logo.png b/docs/img/logo.png new file mode 100644 index 0000000..2ede737 Binary files /dev/null and b/docs/img/logo.png differ diff --git a/go.mod b/go.mod index 2243767..cf7924b 100644 --- a/go.mod +++ b/go.mod @@ -5,21 +5,36 @@ go 1.25.0 require ( github.com/alecthomas/kingpin/v2 v2.4.0 github.com/creasty/defaults v1.8.0 + github.com/gorilla/mux v1.8.1 + github.com/gorilla/websocket v1.5.3 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/pkg/errors v0.9.1 + github.com/samber/lo v1.52.0 + github.com/sourcegraph/conc v0.3.0 google.golang.org/grpc v1.76.0 google.golang.org/protobuf v1.36.6 gopkg.in/yaml.v3 v3.0.1 ) require ( + atomicgo.dev/cursor v0.2.0 // indirect + atomicgo.dev/keyboard v0.2.9 // indirect + atomicgo.dev/schedule v0.1.0 // indirect github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect - github.com/gorilla/mux v1.8.1 // indirect - github.com/rs/cors v1.11.1 // indirect - github.com/samber/lo v1.52.0 // indirect + github.com/containerd/console v1.0.5 // indirect + github.com/gookit/color v1.5.4 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/lithammer/fuzzysearch v1.1.8 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/pterm/pterm v0.12.82 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/net v0.42.0 // indirect golang.org/x/sys v0.34.0 // indirect + golang.org/x/term v0.33.0 // indirect golang.org/x/text v0.27.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect ) diff --git a/go.sum b/go.sum index bd493b3..c797824 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,30 @@ +atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= +atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= +atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= +atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= +atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= +atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= +github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= +github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= +github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= +github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= +github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= +github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= +github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk= github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -37,18 +54,37 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -56,23 +92,43 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= -github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= +github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= +github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= +github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= +github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= +github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= +github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= +github.com/pterm/pterm v0.12.82 h1:+D9wYhCaeaK0FIQoZtqbNQuNpe2lB2tajKKsTd5paVQ= +github.com/pterm/pterm v0.12.82/go.mod h1:TyuyrPjnxfwP+ccJdBTeWHtd/e0ybQHkOS/TakajZCw= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= @@ -92,6 +148,7 @@ go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -99,6 +156,8 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -107,6 +166,9 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -115,16 +177,38 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -136,6 +220,8 @@ golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -159,9 +245,13 @@ google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94U google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/observer/cmd/observer/main.go b/observer/cmd/observer/main.go index 4206712..91aab38 100644 --- a/observer/cmd/observer/main.go +++ b/observer/cmd/observer/main.go @@ -3,12 +3,9 @@ package main import ( "context" "github.com/alecthomas/kingpin/v2" - "google.golang.org/grpc" - "google.golang.org/grpc/backoff" - "google.golang.org/grpc/credentials/insecure" + "load_testing/observer/internal/app" "load_testing/observer/internal/config" "load_testing/observer/internal/http" - "load_testing/worker/proto/gen" "log" "os" "os/signal" @@ -35,61 +32,24 @@ func main() { ctx, cancel := context.WithCancel(context.Background()) go shutdown(cancel) - go grpcStart(ctx, cfg.Workers) go func() { time.Sleep(time.Millisecond * 300) //utils.OpenBrowser(fmt.Sprintf("http://localhost:%d", cfg.Port)) }() - srv, err := http.NewHTTP(cfg.Port, cfg.Workers) - if err != nil { - log.Fatal(err) - } + srv := http.NewHTTP(cfg.Port) + observ := app.NewObserver(srv, cfg.Workers) + err = srv.InitRouts(observ) + check(err) - err = srv.Run(ctx) - if err != nil { - log.Fatal(err) - } + go observ.Run(ctx) + check(srv.Run(ctx)) } -func grpcStart(ctx context.Context, workers []config.WorkerConfig) { - params := grpc.ConnectParams{ - Backoff: backoff.Config{ - BaseDelay: 1 * time.Second, - Multiplier: 1.6, - MaxDelay: 10 * time.Second, - }, - MinConnectTimeout: 5 * time.Second, - } - - withTransport := grpc.WithTransportCredentials(insecure.NewCredentials()) - conn, err := grpc.NewClient(":63546", withTransport, grpc.WithConnectParams(params)) +func check(err error) { if err != nil { - log.Println(err) - return - } - defer conn.Close() - - client := gen.NewWorkerClient(conn) - - for { - stream, err := client.Health(context.Background(), &gen.Empty{}) - if err != nil { - log.Println(err) - time.Sleep(time.Second) - continue - } - - for { - msg, err := stream.Recv() - if err == nil { - log.Println(msg.Status) - } else { - log.Println("ERROR:", err) - break - } - } + log.Fatal(err) } } diff --git a/observer/config.yaml b/observer/config.yaml index bb3a2a5..e1559a2 100644 --- a/observer/config.yaml +++ b/observer/config.yaml @@ -1,5 +1,5 @@ -app_port: 8090 +app_port: 8091 workers: - - addr: "127.0.0.1:55555" + #- addr: "127.0.0.1:55555" - addr: "127.0.0.1:55556" - addr: "127.0.0.1:55557" diff --git a/observer/internal/app/model.go b/observer/internal/app/model.go new file mode 100644 index 0000000..4879f7a --- /dev/null +++ b/observer/internal/app/model.go @@ -0,0 +1 @@ +package app diff --git a/observer/internal/app/observer.go b/observer/internal/app/observer.go index 5d721f2..4f3cbcb 100644 --- a/observer/internal/app/observer.go +++ b/observer/internal/app/observer.go @@ -1,12 +1,100 @@ package app +import ( + "context" + "github.com/samber/lo" + "github.com/sourcegraph/conc" + "load_testing/observer/internal/config" + "load_testing/worker/proto/gen" + "log" +) + +type state string + +const ( + stateReady state = "ready" + stateRunning state = "running" + stateOffline state = "offline" + stateError state = "error" +) + +type WS interface { + WriteWSMessage(msg string) error +} + +type WorkerStatus struct { + workerID int + status gen.WorkerStatus +} + type observer struct { + workers []*Worker } -func NewObserver() *observer { - return &observer{} +func NewObserver(ws WS, workersConf []config.WorkerConfig) *observer { + workers := lo.Map(workersConf, func(item config.WorkerConfig, index int) *Worker { + return &Worker{ + Id: index + 1, + ParallelTests: 1, + Addr: item.Addr, + Status: stateOffline, + ws: ws, + } + }) + + return &observer{ + workers: workers, + } } -func (o *observer) Run() { +func (o *observer) Run(ctx context.Context) { + statusMap := map[gen.WorkerStatus]state{ + gen.WorkerStatus_STATE_READY: stateReady, + gen.WorkerStatus_STATE_RUNNING: stateRunning, + gen.WorkerStatus_STATE_ERROR: stateError, + } + log.Printf("observer starting. Workers - %d", len(o.workers)) + + chanStatus := make(chan WorkerStatus) + var wg conc.WaitGroup + + for _, worker := range o.workers { + wg.Go(func() { + worker.grpcStart(ctx, chanStatus) + }) + } + + go func() { + for state := range chanStatus { + for _, worker := range o.workers { + if worker.Id == state.workerID { + newState := stateOffline + if v, ok := statusMap[state.status]; ok { + newState = v + } + + worker.ChangeState(newState) + } + } + } + }() + + wg.Wait() + close(chanStatus) +} + +func (o *observer) Workers() []*Worker { + return o.workers +} + +func (o *observer) WorkerByID(id int) *Worker { + w, ok := lo.Find(o.workers, func(item *Worker) bool { + return item.Id == id + }) + if !ok { + return nil + } + + return w } diff --git a/observer/internal/app/worker.go b/observer/internal/app/worker.go new file mode 100644 index 0000000..b06b53d --- /dev/null +++ b/observer/internal/app/worker.go @@ -0,0 +1,126 @@ +package app + +import ( + "context" + "encoding/json" + "fmt" + "github.com/pkg/errors" + "google.golang.org/grpc" + "google.golang.org/grpc/backoff" + "google.golang.org/grpc/credentials/insecure" + "load_testing/worker/proto/gen" + "log" + "sync" + "time" +) + +type Worker struct { + Id int `json:"id"` + Addr string `json:"addr"` + Status state `json:"status"` + Script string `json:"script"` + ParallelTests int `json:"parallel_tests"` + ws WS `json:"-"` + client gen.WorkerClient `json:"-"` + mx sync.Mutex `json:"-"` +} + +func (w *Worker) ChangeState(newState state) { + w.mx.Lock() + defer w.mx.Unlock() + + w.Status = newState + + data, _ := json.Marshal(w) + if err := w.ws.WriteWSMessage(string(data)); err != nil { + log.Println("WriteWSMessage error:", err) + } +} + +func (w *Worker) Start(testCount int32) error { + w.mx.Lock() + + if w.Status != stateReady && w.Status != stateError { + w.mx.Unlock() + return fmt.Errorf("incorrect worker status. Current state is %s", w.Status) + } + w.mx.Unlock() + + w.ChangeState(stateRunning) + + go func() { + _, err := w.client.Start(context.Background(), &gen.StartResp{TestCount: testCount}) + if err != nil { + log.Println("start error", err) + } + }() + + return nil +} + +func (w *Worker) Stop() error { + _, err := w.client.Stop(context.Background(), &gen.Empty{}) + return err +} + +func (w *Worker) SetTestScript(script string) error { + w.Script = script + _, err := w.client.SetTestScript(context.Background(), &gen.SetTestScriptReq{Script: script}) + return err +} + +func (w *Worker) grpcStart(ctx context.Context, chanStatus chan<- WorkerStatus) { + params := grpc.ConnectParams{ + Backoff: backoff.Config{ + BaseDelay: 1 * time.Second, + Multiplier: 1.6, + MaxDelay: 10 * time.Second, + }, + MinConnectTimeout: 5 * time.Second, + } + + log.Printf("start GRPC worker %s", w.Addr) + + withTransport := grpc.WithTransportCredentials(insecure.NewCredentials()) + conn, err := grpc.NewClient(w.Addr, withTransport, grpc.WithConnectParams(params)) + if err != nil { + log.Println(errors.Wrap(err, "grpc newClient").Error()) + return + } + defer conn.Close() + + w.client = gen.NewWorkerClient(conn) + w.grpcKeepalive(ctx, w.client, chanStatus) +} + +func (w *Worker) grpcKeepalive(ctx context.Context, client gen.WorkerClient, chanStatus chan<- WorkerStatus) { + for { + stream, err := client.ObserverChangeState(ctx, &gen.Empty{}) + if err != nil { + log.Println("GRPC error:", err) + } else { + chanStatus <- WorkerStatus{workerID: w.Id, status: gen.WorkerStatus_STATE_READY} + w.readStream(stream, w.Id, chanStatus) + } + + chanStatus <- WorkerStatus{workerID: w.Id, status: -1} + select { + case <-ctx.Done(): + log.Println("GRPC: Context done") + return + case <-time.After(time.Second * 3): + } + } +} + +func (w *Worker) readStream(stream grpc.ServerStreamingClient[gen.StatusInfo], workerId int, chanStatus chan<- WorkerStatus) { + for { + msg, err := stream.Recv() + if err == nil { + chanStatus <- WorkerStatus{workerID: workerId, status: msg.Status} + } else { + log.Println("stream ERROR:", err) + break + } + } +} diff --git a/observer/internal/http/handler.go b/observer/internal/http/handler.go index b8a5dd4..e89f976 100644 --- a/observer/internal/http/handler.go +++ b/observer/internal/http/handler.go @@ -1,44 +1,114 @@ package http import ( + "context" "encoding/json" - "github.com/samber/lo" + "fmt" + "github.com/gorilla/mux" + "github.com/pkg/errors" "html/template" + "io" "load_testing/observer" - "load_testing/observer/internal/config" "net/http" + "strconv" ) -func (h *HttpSrv) workers(w http.ResponseWriter, r *http.Request) { - workers := lo.Map(h.workersConf, func(item config.WorkerConfig, index int) WorkerDTO { - return WorkerDTO{ - Id: index + 1, - Addr: item.Addr, - Status: "ready", - Online: true, +func (h *HttpSrv) workers(workers IObserver) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + data, err := json.Marshal(workers.Workers()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return } - }) - data, err := json.Marshal(workers) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - _, _ = w.Write(data) + _, _ = w.Write(data) + } +} + +func (h *HttpSrv) workerStart(workers IObserver) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + idStr := vars["id"] + testCountStr := r.URL.Query().Get("testCount") + id, _ := strconv.Atoi(idStr) + testCount, _ := strconv.Atoi(testCountStr) + + if worker := workers.WorkerByID(id); worker != nil { + if err := worker.Start(int32(max(testCount, 1))); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } else { + http.Error(w, fmt.Sprintf("worker not found by ID: %s", idStr), http.StatusInternalServerError) + } + } +} + +func (h *HttpSrv) workerStop(workers IObserver) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + idStr := vars["id"] + id, _ := strconv.Atoi(vars["id"]) + + if worker := workers.WorkerByID(id); worker != nil { + if err := worker.Stop(); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } else { + http.Error(w, fmt.Sprintf("worker not found by ID: %s", idStr), http.StatusInternalServerError) + } + } } func (h *HttpSrv) openWS(w http.ResponseWriter, r *http.Request) { + h.mx.Lock() + defer h.mx.Unlock() -} - -func (h *HttpSrv) index(w http.ResponseWriter, r *http.Request) { - data := map[string]any{ - "Workers": h.workersConf, + if h.ws != nil && !h.ws.closed.Load() { + return } - tmpl := template.Must(template.ParseFS(observer.StaticFS, "static/templates/index.html")) - - if err := tmpl.Execute(w, data); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + h.ws = NewWSServer(context.Background()) + err := h.ws.Upgrade(w, r, nil) + if err != nil { + http.Error(w, errors.Wrap(err, "ws open").Error(), http.StatusInternalServerError) + return + } +} + +func (h *HttpSrv) index(workers IObserver) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + data := map[string]any{ + "Workers": workers.Workers(), + } + + tmpl := template.Must(template.ParseFS(observer.StaticFS, "static/templates/index.html")) + + if err := tmpl.Execute(w, data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } +} + +func (h *HttpSrv) setScript(workers IObserver) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusInternalServerError) + return + } + + data, _ := io.ReadAll(r.Body) + defer r.Body.Close() + + vars := mux.Vars(r) + idStr := vars["id"] + id, _ := strconv.Atoi(idStr) + + if worker := workers.WorkerByID(id); worker != nil { + if err := worker.SetTestScript(string(data)); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } else { + http.Error(w, fmt.Sprintf("worker not found by ID: %s", idStr), http.StatusInternalServerError) + } } } diff --git a/observer/internal/http/middlewares.go b/observer/internal/http/middlewares.go index 39841db..bfd47e9 100644 --- a/observer/internal/http/middlewares.go +++ b/observer/internal/http/middlewares.go @@ -32,6 +32,11 @@ func loggingMiddleware(logger *slog.Logger) func(next http.Handler) http.Handler return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.String() == "/api/v1/ws" { // исключение для ws + next.ServeHTTP(w, r) + return + } + start := time.Now() logger.DebugContext(r.Context(), fmt.Sprintf("HTTP request started (method: %s, url: %s)", r.Method, r.URL.String())) @@ -61,3 +66,17 @@ func (lw *loggingResponseWriter) WriteHeader(statusCode int) { lw.status = statusCode lw.ResponseWriter.WriteHeader(statusCode) } + +func sendWorkersStatus(workers IObserver) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r) + + // после первого подключения ws отправляем статус по workers + for _, worker := range workers.Workers() { + worker.ChangeState(worker.Status) + } + }) + } +} diff --git a/observer/internal/http/srv.go b/observer/internal/http/srv.go index d6c2be7..d2fd9be 100644 --- a/observer/internal/http/srv.go +++ b/observer/internal/http/srv.go @@ -7,53 +7,64 @@ import ( "github.com/pkg/errors" "io/fs" "load_testing/observer" - "load_testing/observer/internal/config" + "load_testing/observer/internal/app" "log" "log/slog" "net/http" + "sync" "time" ) -type HttpSrv struct { - port int - workersConf []config.WorkerConfig - logger *slog.Logger - httpServ *http.Server +type IObserver interface { + Workers() []*app.Worker + WorkerByID(id int) *app.Worker } -func NewHTTP(port int, workers []config.WorkerConfig) (*HttpSrv, error) { - mux := mux.NewRouter() +type HttpSrv struct { + port int + logger *slog.Logger + httpServ *http.Server + ws *WSServer + mx sync.Mutex + router *mux.Router +} + +func NewHTTP(port int) *HttpSrv { + router := mux.NewRouter() resp := &HttpSrv{ - port: port, - workersConf: workers, - logger: slog.Default(), + port: port, + router: router, + logger: slog.Default(), httpServ: &http.Server{ Addr: fmt.Sprintf(":%d", port), - Handler: mux, + Handler: router, ReadHeaderTimeout: 2 * time.Second, }, } - return resp, resp.initRouts(mux) + return resp } -func (h *HttpSrv) initRouts(mux *mux.Router) error { +func (h *HttpSrv) InitRouts(workers IObserver) error { staticFS, err := fs.Sub(observer.StaticFS, "static") if err != nil { return err } - mux.Use(loggingMiddleware(h.logger)) - mux.Use(enableCORS) + h.router.Use(loggingMiddleware(h.logger)) + h.router.Use(enableCORS) // Отдаём статику (CSS/JS) - mux.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(staticFS)))) - mux.HandleFunc("/", h.index) + h.router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(staticFS)))) + h.router.HandleFunc("/", h.index(workers)) - v1 := mux.PathPrefix("/api/v1").Subrouter() - v1.HandleFunc("/workers", h.workers) - v1.HandleFunc("/ws", h.openWS) + v1 := h.router.PathPrefix("/api/v1").Subrouter() + v1.HandleFunc("/workers", h.workers(workers)) + v1.HandleFunc("/workers/{id}/start", h.workerStart(workers)) + v1.HandleFunc("/workers/{id}/stop", h.workerStop(workers)) + v1.HandleFunc("/workers/{id}/set_script", h.setScript(workers)) + v1.Handle("/ws", sendWorkersStatus(workers)(http.HandlerFunc(h.openWS))) return nil } @@ -67,10 +78,17 @@ func (h *HttpSrv) Run(ctx context.Context) error { h.httpServ.Shutdown(sdCtx) }() - log.Printf("observer starting on port %d. Workers - %d", h.port, len(h.workersConf)) + log.Printf("http starting on port %d", h.port) if err := h.httpServ.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { return err } return nil } + +func (h *HttpSrv) WriteWSMessage(msg string) error { + if h.ws != nil && !h.ws.closed.Load() { + return h.ws.WriteMsg(msg) + } + return nil +} diff --git a/observer/internal/http/websocketServer.go b/observer/internal/http/websocketServer.go new file mode 100644 index 0000000..6cc04d9 --- /dev/null +++ b/observer/internal/http/websocketServer.go @@ -0,0 +1,144 @@ +package http + +import ( + "context" + "log" + "net/http" + "sync" + "sync/atomic" + "time" + + "github.com/gorilla/websocket" + "github.com/pkg/errors" +) + +type WSServer struct { + upgrader *websocket.Upgrader + conn *websocket.Conn + ctx context.Context + close context.CancelFunc + fail atomic.Int32 + lastMsg time.Time + mx sync.RWMutex + writeMx sync.Mutex + closed atomic.Bool +} + +const ( + failCount = 2 +) + +func NewWSServer(pctx context.Context) *WSServer { + upgrader := &websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, + } + + log.Println("WS: new ws conn") + + ctx, cancel := context.WithCancel(pctx) + srv := &WSServer{ + upgrader: upgrader, + ctx: ctx, + close: cancel, + } + + return srv +} + +func (ws *WSServer) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) error { + var err error + + ws.conn, err = ws.upgrader.Upgrade(w, r, responseHeader) + if err == nil { + go ws.readMsg() + } + + return err +} + +func (ws *WSServer) Close() { + if ws.closed.CompareAndSwap(false, true) { + ws.conn.Close() + ws.close() + } +} + +func (ws *WSServer) readMsg() { + msgChan := make(chan struct{}) + defer close(msgChan) + + // контроль сообщений + go func(msg <-chan struct{}) { + for { + select { + case <-ws.ctx.Done(): + log.Println("WS: context done") + ws.Close() + return + case <-time.Tick(time.Second * 10): + // если не получаем сообщения 10 секунд, закрываем конект (фронт переоткроет). Пинги должны приходить раз в 5 сек. + + ws.mx.RLock() + if time.Since(ws.lastMsg).Seconds() >= 10 { + if count := ws.fail.Add(1); count >= failCount { + log.Println("WS: forcibly closing the ws connection") + ws.Close() + } + } + ws.mx.RUnlock() + case <-msg: + ws.mx.Lock() + ws.lastMsg = time.Now() + ws.mx.Unlock() + } + } + }(msgChan) + + for { + select { + case <-ws.ctx.Done(): + log.Println("WS: context done") + return + default: + } + + msgType, _, err := ws.conn.ReadMessage() + if err != nil { + log.Println(errors.Wrap(err, "ws read error")) + ws.Close() + break + } + + if msgType == -1 { // разрыв сокета + ws.Close() + log.Println("websocket closed") + break + } + + msgChan <- struct{}{} + } + +} + +func (ws *WSServer) WriteMsg(data string) error { + ws.writeMx.Lock() + defer ws.writeMx.Unlock() + + if ws.conn == nil { + return errors.New("ws connection not initialized") + } + + err := ws.conn.WriteMessage(websocket.TextMessage, []byte(data)) + return err +} + +func (ws *WSServer) WriteByteMsg(data []byte) error { + if ws.conn == nil { + return errors.New("ws connection not initialized") + } + + err := ws.conn.WriteMessage(websocket.BinaryMessage, data) + return err +} diff --git a/observer/static/app.js b/observer/static/app.js index e802849..8c05732 100644 --- a/observer/static/app.js +++ b/observer/static/app.js @@ -3,41 +3,42 @@ let workers = []; let testRunning = false; let testStartTime = null; let timerInterval = null; -let back_api = "http://localhost:8090/api/v1" +let back_api = "http://localhost:8091/api/v1" // Initialize the application function init() { + const ws = openWSConn([]); + getWorkers(() => { - renderWorkers(); + renderWorkers(ws); populateWorkerSelects(); setupEventListeners(); - updateGlobalMetrics(); - updateGlobalStatus(); - updateTotalCapacity(); }); } // Render worker cards -function renderWorkers() { +function renderWorkers(ws) { const workersGrid = document.getElementById('workersGrid'); workersGrid.innerHTML = ''; workers.forEach(worker => { - const card = createWorkerCard(worker); + const card = createWorkerCard(worker, ws); workersGrid.appendChild(card); + + updateWorkerStatus(worker) + updateButton(worker) + updateLastHeartbeat(worker) }); } // Create a worker card element -function createWorkerCard(worker) { +function createWorkerCard(worker, ws) { const card = document.createElement('div'); card.className = `worker-card status-${worker.status}`; card.dataset.workerId = worker.id; - const statusText = worker?.status?.charAt(0).toUpperCase() + worker?.status?.slice(1); const isRunning = worker?.status === 'running'; const parallelValueClass = worker?.parallel_tests > 500 ? 'parallel-value warning' : 'parallel-value'; - const progressPercent = isRunning && worker?.parallel_tests > 0 ? (worker?.active_parallel / worker?.parallel_tests * 100) : 0; card.innerHTML = `
@@ -45,15 +46,15 @@ function createWorkerCard(worker) {

Worker ${worker.id}

${worker.addr}
-
+
- ${statusText} + ${worker.status}
Last Heartbeat: - ${worker.last_heartbeat} +
@@ -68,18 +69,18 @@ function createWorkerCard(worker) { Parallel Tests ${isRunning ? ` -
- - - - - Stop worker to adjust settings -
+ + + + + + + ` : ''}
- -
${worker.parallel_tests}
- + +
${worker.parallel_tests}
+
${worker.parallel_tests > 500 ? `
@@ -91,47 +92,22 @@ function createWorkerCard(worker) { High load warning
` : ''} - ${isRunning ? ` -
-
- Active Virtual Users - ${worker.active_parallel}/${worker.parallel_tests} -
-
-
-
-
- ` : ''} -
-
-
Req/s
-
${worker?.metrics?.requests_per_sec}
-
-
-
Avg RT
-
${worker?.metrics?.avg_response_time}ms
-
-
-
Errors
-
${worker?.metrics?.error_rate}%
-
-
- - -
`; + // подписываемся на сообщение в ws по текущему воркеру + if (ws !== undefined) { + ws((m) => { + if (m?.id === worker.id) { + updateWorkerStatus(m) + updateButton(m) + updateLastHeartbeat(m) + } + }) + } + return card; } +function updateLastHeartbeat(worker) { + const now = new Date(); + const pad = n => n.toString().padStart(2, '0'); + + const lastHeartbeat = document.getElementById(`last_heartbeat-${worker.id}`); + if (lastHeartbeat) { + lastHeartbeat.textContent = `${pad(now.getDate())}-${pad(now.getMonth())}-${now.getFullYear()} ${pad(now.getHours())}:${pad(now.getMinutes())}` + } +} + +function updateButton(worker) { + const buttonConfig = document.getElementById(`button-config-${worker.id}`); + const buttonStop = document.getElementById(`button-stop-${worker.id}`); + const buttonStart = document.getElementById(`button-start-${worker.id}`); + + if (buttonConfig) { + buttonConfig.disabled = worker.status !== "ready" && worker.status !== "error" + } + if (buttonStop) { + buttonStop.disabled = worker.status !== "running" + } + if (buttonStart) { + buttonStart.disabled = worker.status !== "ready" && worker.status !== "error" + } + + // глобальные кнопки + const allStart = document.getElementById(`startAllBtn`); + const allStop = document.getElementById(`stopAllBtn`); + if(allStart) { + allStart.disabled = !workers.some(w => w.status === "ready" || w.status === "error") + } + if(allStop) { + allStop.disabled = !workers.some(w => w.status === "running") + } +} + +function updateWorkerStatus(worker) { + const workerElem = document.getElementById(`worker-state-${worker.id}`); + if (workerElem) { + workerElem.textContent = worker.status; + workerElem.className = `worker-status-badge status-${worker.status}` + + const span = document.createElement('span'); + span.className = `status-dot status-${worker.status}`; + workerElem.prepend(span); + + document.querySelectorAll(`div[class^="worker-card"][data-worker-id="${worker.id}"]`).forEach(e => { + e.setAttribute('class', ''); + e.classList.add(`worker-card`); + e.classList.add(`status-${worker.status}`); + }) + + + workers.forEach(w => { + if (w.id === worker.id) { + w.status = worker.status + } + } ) + } +} + // Populate worker selects and checkboxes function populateWorkerSelects() { const workerSelect = document.getElementById('workerSelect'); @@ -155,10 +203,6 @@ function populateWorkerSelects() { const option = document.createElement('option'); option.value = worker.id; option.textContent = `Worker ${worker.id} (${worker.addr})`; - if (!worker.online) { - option.disabled = true; - option.textContent += ' - Offline'; - } workerSelect.appendChild(option); }); @@ -168,7 +212,7 @@ function populateWorkerSelects() { const checkboxDiv = document.createElement('div'); checkboxDiv.className = 'worker-checkbox'; checkboxDiv.innerHTML = ` - + `; workersChecklist.appendChild(checkboxDiv); @@ -191,20 +235,14 @@ function setupEventListeners() { document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', (e) => switchTab(e.target.dataset.tab)); }); - - // Config tab switching - // document.querySelectorAll('.config-tab-btn').forEach(btn => { - // btn.addEventListener('click', (e) => switchConfigTab(e.target.dataset.configTab)); + + // // Preset buttons + // document.querySelectorAll('.preset-btn').forEach(btn => { + // btn.addEventListener('click', (e) => { + // const presetValue = parseInt(e.currentTarget.dataset.preset); + // }); // }); - - // Preset buttons - document.querySelectorAll('.preset-btn').forEach(btn => { - btn.addEventListener('click', (e) => { - const presetValue = parseInt(e.currentTarget.dataset.preset); - applyPreset(presetValue); - }); - }); - + // // Apply to all button document.getElementById('applyToAllBtn').addEventListener('click', applyToAll); @@ -267,19 +305,10 @@ function toggleParallelConfigSection() { toggleBtn.classList.toggle('collapsed'); } -// // Switch config tabs -// function switchConfigTab(tab) { -// document.querySelectorAll('.config-tab-btn').forEach(btn => btn.classList.remove('active')); -// document.querySelectorAll('.config-tab-content').forEach(content => content.classList.remove('active')); -// -// document.querySelector(`[data-config-tab="${tab}"]`).classList.add('active'); -// document.getElementById(`${tab}Tab`).classList.add('active'); -// } - // Adjust parallel tests for a worker function adjustParallelTests(workerId, action) { const worker = workers.find(w => w.id === workerId); - if (!worker || !worker.online || worker.status === 'running') return; + if (!worker || worker.status !== 'ready') return; const increment = action === 'increment' ? 1 : -1; const newValue = worker.parallel_tests + increment; @@ -290,9 +319,9 @@ function adjustParallelTests(workerId, action) { } worker.parallel_tests = newValue; - renderWorkers(); - updateTotalCapacity(); - + const elem = document.getElementById(`parallel_tests-${worker.id}`); + if (elem) elem.textContent = newValue + const card = document.querySelector(`[data-worker-id="${workerId}"]`); card.style.animation = 'none'; setTimeout(() => { @@ -300,26 +329,6 @@ function adjustParallelTests(workerId, action) { }, 10); } -// Apply preset to all workers -function applyPreset(value) { - const onlineWorkers = workers.filter(w => w.online).length; - - showModal( - 'Apply Preset', - `Set ${value} parallel tests for all ${onlineWorkers} online worker(s)?`, - () => { - workers.forEach(worker => { - if (worker.online) { - worker.parallel_tests = value; - } - }); - renderWorkers(); - updateTotalCapacity(); - showNotification(`Preset applied: ${value} parallel tests per worker`, 'success'); - } - ); -} - // Apply custom value to all workers function applyToAll() { const input = document.getElementById('batchParallelInput'); @@ -329,40 +338,16 @@ function applyToAll() { showNotification('Please enter a value between 1 and 1000!', 'error'); return; } - - const onlineWorkers = workers.filter(w => w.online).length; - - // showModal( - // 'Apply to All Workers', - // `Set ${value} parallel tests for all ${onlineWorkers} online worker(s)?`, - // () => { - // workers.forEach(worker => { - // if (worker.online) { - // worker.parallel_tests = value; - // } - // }); - // renderWorkers(); - // updateTotalCapacity(); - // showNotification(`Applied ${value} parallel tests to all online workers`, 'success'); - // } - // ); workers.forEach(worker => { - if (worker.online) { + if (worker.status === "ready") { worker.parallel_tests = value; } }); renderWorkers(); - //updateTotalCapacity(); showNotification(`Applied ${value} parallel tests to all online workers`, 'success'); } -// Update total capacity display -// function updateTotalCapacity() { -// const totalCapacity = workers.reduce((sum, w) => sum + w.parallel_tests, 0); -// document.getElementById('totalCapacity').textContent = totalCapacity; -// } - // Switch tabs function switchTab(tab) { document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active')); @@ -378,96 +363,39 @@ function startAllWorkers() { const stopBtn = document.getElementById('stopAllBtn'); startBtn.disabled = true; - startBtn.classList.add('loading'); - setTimeout(() => { - workers.forEach(worker => { - if (worker.online && worker.status === 'ready') { - worker.status = 'running'; - if (worker?.metrics?.requests_per_sec === 0) { - worker.metrics.requests_per_sec = Math.floor(Math.random() * 150) + 100; - worker.metrics.avg_response_time = Math.floor(Math.random() * 50) + 30; - worker.metrics.error_rate = (Math.random() * 1).toFixed(1); - } - } - }); - - testRunning = true; - testStartTime = Date.now(); - startTimer(); - - renderWorkers(); - updateGlobalStatus(); - updateGlobalMetrics(); - updateTotalCapacity(); - - startBtn.classList.remove('loading'); - stopBtn.disabled = false; - - showNotification('All workers started successfully!', 'success'); - }, 1500); + workers.forEach(worker => { + if (worker.status === 'ready' || worker.status === 'error') { + startWorker(worker.id) + } + }); + showNotification('All workers started successfully!', 'success'); } // Stop all workers function stopAllWorkers() { - const startBtn = document.getElementById('startAllBtn'); + //const startBtn = document.getElementById('startAllBtn'); const stopBtn = document.getElementById('stopAllBtn'); stopBtn.disabled = true; - stopBtn.classList.add('loading'); - setTimeout(() => { - workers.forEach(worker => { - if (worker.status === 'running') { - worker.status = 'ready'; - } - }); - - testRunning = false; - stopTimer(); - - renderWorkers(); - updateGlobalStatus(); - updateGlobalMetrics(); - updateTotalCapacity(); - - stopBtn.classList.remove('loading'); - startBtn.disabled = false; - - showNotification('All workers stopped!', 'warning'); - }, 1000); + workers.forEach(worker => { + if (worker.status === 'running') { + stopWorker(worker.id) + } + }); + showNotification('All workers stopped!', 'warning'); } // Start individual worker function startWorker(workerId) { const worker = workers.find(w => w.id === workerId); - if (!worker || !worker.online) return; + if (!worker) return; - const button = document.querySelector(`.start-worker[data-worker-id="${workerId}"]`); - button.disabled = true; - button.classList.add('loading'); + const buttonStart = document.getElementById(`button-start-${worker.id}`); + buttonStart.disabled = true - setTimeout(() => { - worker.status = 'running'; - worker.metrics.requests_per_sec = Math.floor(Math.random() * 150) + 100; - worker.metrics.avg_response_time = Math.floor(Math.random() * 50) + 30; - worker.metrics.error_rate = (Math.random() * 1).toFixed(1); - // Simulate active parallel tests (80-100% of configured) - worker.active_parallel = Math.floor(worker.parallel_tests * (0.8 + Math.random() * 0.2)); - - if (!testRunning) { - testRunning = true; - testStartTime = Date.now(); - startTimer(); - } - - renderWorkers(); - updateGlobalStatus(); - updateGlobalMetrics(); - updateTotalCapacity(); - - showNotification(`Worker ${workerId} started!`, 'success'); - }, 800); + fetch(`${back_api}/workers/${workerId}/start?testCount=${worker.parallel_tests}`).then(r => buttonStart.disabled = false ) } // Stop individual worker @@ -475,27 +403,7 @@ function stopWorker(workerId) { const worker = workers.find(w => w.id === workerId); if (!worker) return; - const button = document.querySelector(`.stop-worker[data-worker-id="${workerId}"]`); - button.disabled = true; - button.classList.add('loading'); - - setTimeout(() => { - worker.status = 'ready'; - - // Check if any workers are still running - const anyRunning = workers.some(w => w.status === 'running'); - if (!anyRunning) { - testRunning = false; - stopTimer(); - } - - renderWorkers(); - updateGlobalStatus(); - updateGlobalMetrics(); - updateTotalCapacity(); - - showNotification(`Worker ${workerId} stopped!`, 'warning'); - }, 800); + fetch(`${back_api}/workers/${workerId}/stop`).then(r => {} ) } // Configure worker @@ -516,13 +424,16 @@ function configureWorker(workerId) { toggleScriptSection(); } + const scriptElem = document.getElementById('individualScript'); + if(scriptElem) scriptElem.textContent = worker.script; + showNotification(`Ready to configure Worker ${workerId}`, 'info'); } // Deploy script to individual worker function deployIndividualScript() { const workerSelect = document.getElementById('workerSelect'); - const script = document.getElementById('individualScript').value; + const script = document.getElementById('individualScript')?.value; const workerId = parseInt(workerSelect.value); if (!workerId) { @@ -536,14 +447,12 @@ function deployIndividualScript() { } const worker = workers.find(w => w.id === workerId); - const scriptName = `Script_${Date.now()}`; - showModal( - 'Deploy Script', + showModal('Deploy Script', `Deploy script to Worker ${workerId} (${worker.addr})?`, () => { - worker.script_name = scriptName; - renderWorkers(); + worker.script = script; + setScript(worker.id, script); showNotification(`Script deployed to Worker ${workerId}!`, 'success'); } ); @@ -564,91 +473,18 @@ function deployBatchScript() { return; } - const scriptName = `Batch_Script_${Date.now()}`; const workerIds = Array.from(selectedCheckboxes).map(cb => parseInt(cb.value)); - showModal( - 'Deploy Script', - `Deploy script to ${workerIds.length} worker(s)?`, - () => { - workerIds.forEach(id => { - const worker = workers.find(w => w.id === id); - if (worker) { - worker.script_name = scriptName; - } - }); - renderWorkers(); - showNotification(`Script deployed to ${workerIds.length} worker(s)!`, 'success'); - } - ); -} + showModal('Deploy Script',`Deploy script to ${workerIds.length} worker(s)?`, () => { + workers.forEach(w => { + if(workerIds.some(id => w.id === id)) { + w.script = script; + setScript(w.id, script) + } + }) + }); -// Update global status -function updateGlobalStatus() { - const globalStatus = document.getElementById('globalStatus'); - const runningCount = workers.filter(w => w.status === 'running').length; - - if (testRunning && runningCount > 0) { - globalStatus.innerHTML = ` - - Test Running (${runningCount} active) - `; - } else { - globalStatus.innerHTML = ` - - System Ready - `; - } - - // Update master buttons - const hasReadyWorkers = workers.some(w => w.online && w.status === 'ready'); - const hasRunningWorkers = workers.some(w => w.status === 'running'); - - document.getElementById('startAllBtn').disabled = !hasReadyWorkers || testRunning; - document.getElementById('stopAllBtn').disabled = !hasRunningWorkers; -} - -// Update global metrics -function updateGlobalMetrics() { - const runningWorkers = workers.filter(w => w.status === 'running'); - - const totalRequests = runningWorkers.reduce((sum, w) => sum + w.metrics.requests_per_sec, 0); - const avgResponseTime = runningWorkers.length > 0 - ? Math.round(runningWorkers.reduce((sum, w) => sum + w?.metrics?.avg_response_time, 0) / runningWorkers.length) - : 0; - const avgErrorRate = runningWorkers.length > 0 - ? (runningWorkers.reduce((sum, w) => sum + parseFloat(w?.metrics?.error_rate), 0) / runningWorkers.length).toFixed(1) - : 0; - - document.getElementById('totalRequests').textContent = totalRequests; - document.getElementById('avgResponseTime').textContent = `${avgResponseTime} ms`; - document.getElementById('errorRate').textContent = `${avgErrorRate}%`; -} - -// Timer functions -function startTimer() { - if (timerInterval) return; - - timerInterval = setInterval(() => { - if (!testStartTime) return; - - const elapsed = Date.now() - testStartTime; - const hours = Math.floor(elapsed / 3600000); - const minutes = Math.floor((elapsed % 3600000) / 60000); - const seconds = Math.floor((elapsed % 60000) / 1000); - - const timeString = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; - document.getElementById('testDuration').textContent = timeString; - }, 1000); -} - -function stopTimer() { - if (timerInterval) { - clearInterval(timerInterval); - timerInterval = null; - document.getElementById('testDuration').textContent = '00:00:00'; - testStartTime = null; - } + showNotification(`Script deployed to ${workerIds.length} worker(s)!`, 'success'); } // Modal functions @@ -678,15 +514,58 @@ function showNotification(message, type) { console.log(`[${type.toUpperCase()}] ${message}`); } -function request(method) { - -} - function getWorkers(f) { fetch(`${back_api}/workers`). then(resp => resp.json()). then(w => { workers.push(...w); f() }) } +function openWSConn(listeners) { + const ws = new WebSocket(`${back_api}/ws`); + + ws.onopen = () => { + console.log("✅ WS connected"); + + // запускаем keepalive + let heartbeatRef = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: "ping" })); + return + } + + if (ws.readyState === WebSocket.CLOSED) { + console.log("🔌 write to closed WS"); + clearInterval(heartbeatRef); // очистим старый + setTimeout(() => openWSConn(listeners), 100) + } + }, 5_000); // каждые 5 сек + }; + + ws.onmessage = (event) => { + try { + listeners.forEach((h) => h(JSON.parse(event.data))); // уведомляем подписчиков + } catch (e) { + console.error("Invalid WS message"); + } + }; + + ws.onerror = (err) => { + setTimeout(() => openWSConn(listeners), 5_000) + console.error("❌ WS error", err); + }; + + return (f) => { + listeners.push(f) + } +} + +function setScript(workerId, script) { + fetch(`${back_api}/workers/${workerId}/set_script`, { + method: 'POST', + body: script + }).then(w => {}) +} + + // Initialize on page load document.addEventListener('DOMContentLoaded', init); \ No newline at end of file diff --git a/observer/static/favicon.ico b/observer/static/favicon.ico new file mode 100644 index 0000000..c795498 Binary files /dev/null and b/observer/static/favicon.ico differ diff --git a/observer/static/templates/index.html b/observer/static/templates/index.html index 4bcc19c..46d7a81 100644 --- a/observer/static/templates/index.html +++ b/observer/static/templates/index.html @@ -5,6 +5,7 @@ Load Testing Control Panel + @@ -12,10 +13,6 @@

Панель управление нагрузочным тестом

-
- - -
@@ -23,7 +20,7 @@ {{ len .Workers }}
-
-
+
diff --git a/worker/cmd/worker/main.go b/worker/cmd/worker/main.go index 2900ad4..759138b 100644 --- a/worker/cmd/worker/main.go +++ b/worker/cmd/worker/main.go @@ -4,12 +4,15 @@ package main import ( "context" "github.com/alecthomas/kingpin/v2" + "github.com/pterm/pterm" "load_testing/worker/grpc" "load_testing/worker/internal/app" + "load_testing/worker/internal/utils" "log" "os" "os/signal" "syscall" + "time" ) //go:generate protoc --proto_path=../../proto --go_out=../../proto --go-grpc_out=../../proto worker.proto @@ -35,9 +38,17 @@ func main() { ctx, cancel := context.WithCancel(context.Background()) go shutdown(cancel) - err := grpc.NewGRPCServer(ctx, port, app.NewWorker()) + worker := app.NewWorker() + if err := worker.Init(ctx); err != nil { + pterm.Fatal.WithFatal(true).Println(err.Error()) + } + + time.AfterFunc(time.Second, func() { + pterm.Info.Printf("worker запущен на порту %d", port) + }) + err := grpc.NewGRPCServer(ctx, port, worker) if err != nil { - log.Fatal(err) + pterm.Fatal.WithFatal(true).Println(err.Error()) } } @@ -47,6 +58,6 @@ func shutdown(cancel context.CancelFunc) { signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs - log.Println("shutting down") + utils.Logger().Info("shutting down") cancel() } diff --git a/worker/grpc/server.go b/worker/grpc/server.go index e51e87c..7cf5bcf 100644 --- a/worker/grpc/server.go +++ b/worker/grpc/server.go @@ -5,8 +5,9 @@ import ( "fmt" grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery" "google.golang.org/grpc" + "load_testing/worker/internal/utils" "load_testing/worker/proto/gen" - "log" + "log/slog" "net" ) @@ -17,7 +18,9 @@ func NewGRPCServer(ctx context.Context, port int, worker gen.WorkerServer) error } actualAddr := listener.Addr().String() // ip:port - srv := grpc.NewServer(grpc.ChainStreamInterceptor(grpc_recovery.StreamServerInterceptor())) + logger := utils.Logger().With("name", "grpc") + + srv := grpc.NewServer(grpc.ChainUnaryInterceptor(logInterceptor(logger)), grpc.ChainStreamInterceptor(grpc_recovery.StreamServerInterceptor())) gen.RegisterWorkerServer(srv, worker) go func() { @@ -25,6 +28,19 @@ func NewGRPCServer(ctx context.Context, port int, worker gen.WorkerServer) error srv.Stop() }() - log.Println("сервер запущен на", actualAddr) + logger.InfoContext(ctx, fmt.Sprintf("сервер запущен на %s", actualAddr)) return srv.Serve(listener) } + +func logInterceptor(logger *slog.Logger) func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) { + _, err = handler(ctx, req) + if err != nil { + logger.ErrorContext(ctx, "grpc error", "error", err) + } else { + logger.InfoContext(ctx, fmt.Sprintf("grpc method %s", info.FullMethod)) + } + + return + } +} diff --git a/worker/internal/app/job.go b/worker/internal/app/job.go new file mode 100644 index 0000000..89f54d5 --- /dev/null +++ b/worker/internal/app/job.go @@ -0,0 +1,27 @@ +package app + +import ( + "context" + "fmt" + "github.com/hashicorp/go-multierror" + "github.com/sourcegraph/conc" +) + +func (w *Worker) startJob(ctx context.Context, testCount int32) error { + w.logger.InfoContext(ctx, fmt.Sprintf("start worker, test count %d", testCount)) + + err := new(multierror.Error) + var wg conc.WaitGroup + for range testCount { + wg.Go(func() { + if e := w.runTest(ctx, w.playwrightDir); e != nil { + err = multierror.Append(err, e) + return + } + w.logger.InfoContext(ctx, "test is pass") + }) + } + + wg.Wait() + return err.ErrorOrNil() +} diff --git a/worker/internal/app/playwright.go b/worker/internal/app/playwright.go new file mode 100644 index 0000000..c75d8ba --- /dev/null +++ b/worker/internal/app/playwright.go @@ -0,0 +1,136 @@ +package app + +import ( + "context" + "embed" + "fmt" + "github.com/pkg/errors" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" +) + +//go:embed resource/* +var staticFS embed.FS + +func (w *Worker) runTest(ctx context.Context, playwrightDir string) error { + w.logger.InfoContext(ctx, "exec run playwright test") + + if strings.TrimSpace(w.script) == "" { + return errors.New("script not filled ") + } + + f, err := os.CreateTemp(filepath.Join(playwrightDir, "tests"), "*.spec.js") + if err != nil { + return errors.Wrap(err, "create temp error") + } + _, _ = f.WriteString(w.script) + _ = f.Close() + defer os.Remove(f.Name()) + + _, file := filepath.Split(f.Name()) + cmd := exec.CommandContext(ctx, "npx", "playwright", "test", "tests/"+file, "--project", "chromium") + cmd.Dir = playwrightDir + cmd.Env = append(os.Environ(), "PLAYWRIGHT_HTML_OPEN=never") // что б не открывался отчет в браузере + + _, err = w.cmdRun(ctx, cmd) + return err +} + +func (w *Worker) checkInstall(ctx context.Context) (error, bool) { + cmd := exec.CommandContext(ctx, "npx", "playwright", "test", "--version") + + out, err := w.cmdRun(ctx, cmd) + if err != nil { + return err, false + } + + var re = regexp.MustCompile(`(?m)Version[\s]+[\d\.]+`) + return nil, re.Match(out) +} + +func (w *Worker) install(ctx context.Context) error { + w.logger.InfoContext(ctx, "exec install playwright") + + cmd := exec.CommandContext(ctx, "npx", "playwright", "install") + + _, err := w.cmdRun(ctx, cmd) + if err != nil { + return err + } + + return nil +} + +func (w *Worker) create(ctx context.Context, rootDir string) error { + w.logger.InfoContext(ctx, "exec create-playwright") + + if err := os.Mkdir(rootDir, os.ModeDir); err != nil { + return err + } + + cmd := exec.CommandContext(ctx, "npx", "create-playwright@latest", "--quiet", "--lang", "js", "--install-deps", "--gha") + cmd.Dir = rootDir + + _, err := w.cmdRun(ctx, cmd) + if err != nil { + return err + } + + // Заменяем playwright.config.js на свой + if err := replacePlaywrightConfig(rootDir); err != nil { + return fmt.Errorf("failed to replace config: %w", err) + } + + return nil +} + +func replacePlaywrightConfig(rootDir string) error { + data, err := staticFS.ReadFile("resource/playwright.config.js") + if err != nil { + return err + } + + targetPath := filepath.Join(rootDir, "playwright.config.js") + return os.WriteFile(targetPath, data, 0o644) +} +func (w *Worker) cmdRun(ctx context.Context, cmd *exec.Cmd) ([]byte, error) { + w.stopProcess(ctx, cmd) + + //stdout, err := cmd.StdoutPipe() + //if err != nil { + // return errors.Wrap(err, "stdout pipe error") + //} + // + //if err := cmd.Start(); err != nil { + // return errors.Wrap(err, "command start error") + //} + // + //go func() { + // scanner := bufio.NewScanner(stdout) + // for scanner.Scan() { + // fmt.Println(scanner.Text()) + // } + //}() + // + //if err := cmd.Wait(); err != nil { + // return errors.Wrap(err, "the test failed with an error") + //} + + out, err := cmd.CombinedOutput() + if err != nil { + w.logger.ErrorContext(ctx, "run process error", "playwright error", string(out)) + return out, errors.Wrap(err, "the test failed with an error") + } + + return out, nil +} + +// npx create-playwright@latest --quiet --lang=js --install-deps --gha +// npx playwright install +// npx playwright uninstall --all +// npx playwright --version +// npx playwright codegen http://localhost/bsp +// npx playwright test ./tests/bsp.spec.js --project=chromium --ui diff --git a/worker/internal/app/resource/playwright.config.js b/worker/internal/app/resource/playwright.config.js new file mode 100644 index 0000000..9135f81 --- /dev/null +++ b/worker/internal/app/resource/playwright.config.js @@ -0,0 +1,87 @@ +// @ts-check +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [['html', { outputFolder: 'playwright-report', open: 'never' }]], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + // Делать скриншот при падении теста + + screenshot: 'only-on-failure', + // или: 'on' — для каждого теста + video: 'retain-on-failure', // можно и видео + + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://localhost:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); + diff --git a/worker/internal/app/run_linux.go b/worker/internal/app/run_linux.go new file mode 100644 index 0000000..d2a1cb9 --- /dev/null +++ b/worker/internal/app/run_linux.go @@ -0,0 +1,7 @@ +package app + +import "context" + +func stopProcess(ctx context.Context) { + +} diff --git a/worker/internal/app/run_windows.go b/worker/internal/app/run_windows.go new file mode 100644 index 0000000..c4c34b0 --- /dev/null +++ b/worker/internal/app/run_windows.go @@ -0,0 +1,23 @@ +package app + +import ( + "context" + "golang.org/x/sys/windows" + "os/exec" + "syscall" +) + +// в windows отмена контекста не сразу останавливает процесс, поэтому написана такая функция +func (w *Worker) stopProcess(ctx context.Context, cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{ + CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, + } + + go func() { + <-ctx.Done() + w.logger.WarnContext(ctx, "context canceled -> terminating process group") + + // Отправляем Ctrl-Break всей группе процессов + _ = windows.GenerateConsoleCtrlEvent(syscall.CTRL_BREAK_EVENT, uint32(cmd.Process.Pid)) + }() +} diff --git a/worker/internal/app/worker.go b/worker/internal/app/worker.go index 26ccb2d..fc72e8d 100644 --- a/worker/internal/app/worker.go +++ b/worker/internal/app/worker.go @@ -2,43 +2,112 @@ package app import ( "context" + "github.com/pkg/errors" + "github.com/pterm/pterm" "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" + "load_testing/worker/internal/utils" "load_testing/worker/proto/gen" - "log" - "time" + "log/slog" + "os" + "path/filepath" ) type Worker struct { gen.UnsafeWorkerServer + + state chan gen.WorkerStatus + cancelJob context.CancelFunc + playwrightDir string + script string + logger *slog.Logger } func NewWorker() *Worker { - return &Worker{} + return &Worker{ + state: make(chan gen.WorkerStatus, 1), + cancelJob: func() {}, + logger: utils.Logger().With("name", "worker"), + } } -func (w *Worker) SetTestScript(context.Context, *gen.SetTestScriptReq) (*gen.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method SetTestScript not implemented") -} -func (w *Worker) Start(context.Context, *gen.Empty) (*gen.StartResp, error) { - return nil, status.Errorf(codes.Unimplemented, "method Start not implemented") -} -func (w *Worker) Stop(context.Context, *gen.Empty) (*gen.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method Stop not implemented") -} -func (w *Worker) Health(_ *gen.Empty, stream grpc.ServerStreamingServer[gen.HealthResp]) error { - for { - err := stream.Send(&gen.HealthResp{ - Status: gen.WorkerStatus_READY, - }) +func (w *Worker) Init(ctx context.Context) error { + utils.Logger().Info("init worker") - if err != nil { - log.Println("ERROR:", err) + multi := pterm.DefaultMultiPrinter + multi.Start() + + spinner, _ := pterm.DefaultSpinner.WithWriter(multi.NewWriter()).Start("Установка playwright") + if err := w.install(ctx); err != nil { + spinner.Fail(err.Error()) + return err + } + spinner.Success("playwright установлен") + + dir, _ := os.Getwd() + w.playwrightDir = filepath.Join(dir, "playwright") + if !dirExists(w.playwrightDir) { + spinner, _ := pterm.DefaultSpinner.WithWriter(multi.NewWriter()).Start("Подготовка playwright") + if err := w.create(ctx, w.playwrightDir); err != nil { + spinner.Fail(err.Error()) return err } - time.Sleep(time.Second) + spinner.Success("playwright подготовлен") + } + + multi.Stop() + + return nil +} + +func dirExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +func (w *Worker) SetTestScript(_ context.Context, req *gen.SetTestScriptReq) (*gen.Empty, error) { + w.script = req.Script + + return new(gen.Empty), nil +} + +func (w *Worker) Start(ctxParent context.Context, resp *gen.StartResp) (_ *gen.Empty, err error) { + defer func() { + if err != nil { + w.logger.ErrorContext(ctxParent, errors.Wrap(err, "start error").Error()) + w.state <- gen.WorkerStatus_STATE_ERROR + } else { + w.state <- gen.WorkerStatus_STATE_READY + } + }() + + w.state <- gen.WorkerStatus_STATE_RUNNING + + ctx, cancel := context.WithCancel(ctxParent) + w.cancelJob = cancel + + if err := w.startJob(ctx, resp.TestCount); err != nil { + return nil, err + } + + return new(gen.Empty), nil +} + +func (w *Worker) Stop(context.Context, *gen.Empty) (*gen.Empty, error) { + w.cancelJob() + return new(gen.Empty), nil +} + +func (w *Worker) ObserverChangeState(_ *gen.Empty, stream grpc.ServerStreamingServer[gen.StatusInfo]) error { + for state := range w.state { + err := stream.Send(&gen.StatusInfo{ + Status: state, + }) + + if err != nil { + w.logger.Error(errors.Wrap(err, "stream send error").Error()) + return err + } } return nil diff --git a/worker/internal/utils/logger.go b/worker/internal/utils/logger.go new file mode 100644 index 0000000..680f25f --- /dev/null +++ b/worker/internal/utils/logger.go @@ -0,0 +1,33 @@ +package utils + +import ( + "fmt" + "gopkg.in/natefinch/lumberjack.v2" + "log/slog" + "os" + "path/filepath" + "time" +) + +var ( + logger *slog.Logger +) + +func init() { + logDir, _ := os.Getwd() + rotator := &lumberjack.Logger{ + Filename: filepath.Join(logDir, fmt.Sprintf("%s.log", time.Now().Format("2006-01-02 15-04-05"))), // путь к файлу + MaxSize: 10, // мегабайты до ротации + MaxBackups: 5, // сколько файлов хранить + MaxAge: 30, // дней до удаления старых + Compress: true, // gzip старые логи + } + + logger = slog.New(slog.NewJSONHandler(rotator, &slog.HandlerOptions{ + Level: slog.LevelDebug, + })) +} + +func Logger() *slog.Logger { + return logger +} diff --git a/worker/proto/gen/worker.pb.go b/worker/proto/gen/worker.pb.go index 4afa489..c699952 100644 --- a/worker/proto/gen/worker.pb.go +++ b/worker/proto/gen/worker.pb.go @@ -24,22 +24,25 @@ const ( type WorkerStatus int32 const ( - WorkerStatus_READY WorkerStatus = 0 - WorkerStatus_DONE WorkerStatus = 1 - WorkerStatus_ERROR WorkerStatus = 2 + WorkerStatus_STATE_UNSPECIFIED WorkerStatus = 0 + WorkerStatus_STATE_READY WorkerStatus = 1 + WorkerStatus_STATE_RUNNING WorkerStatus = 2 + WorkerStatus_STATE_ERROR WorkerStatus = 3 ) // Enum value maps for WorkerStatus. var ( WorkerStatus_name = map[int32]string{ - 0: "READY", - 1: "DONE", - 2: "ERROR", + 0: "STATE_UNSPECIFIED", + 1: "STATE_READY", + 2: "STATE_RUNNING", + 3: "STATE_ERROR", } WorkerStatus_value = map[string]int32{ - "READY": 0, - "DONE": 1, - "ERROR": 2, + "STATE_UNSPECIFIED": 0, + "STATE_READY": 1, + "STATE_RUNNING": 2, + "STATE_ERROR": 3, } ) @@ -70,27 +73,27 @@ func (WorkerStatus) EnumDescriptor() ([]byte, []int) { return file_worker_proto_rawDescGZIP(), []int{0} } -type HealthResp struct { +type StatusInfo struct { state protoimpl.MessageState `protogen:"open.v1"` Status WorkerStatus `protobuf:"varint,4,opt,name=status,proto3,enum=WorkerStatus" json:"status,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *HealthResp) Reset() { - *x = HealthResp{} +func (x *StatusInfo) Reset() { + *x = StatusInfo{} mi := &file_worker_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HealthResp) String() string { +func (x *StatusInfo) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HealthResp) ProtoMessage() {} +func (*StatusInfo) ProtoMessage() {} -func (x *HealthResp) ProtoReflect() protoreflect.Message { +func (x *StatusInfo) ProtoReflect() protoreflect.Message { mi := &file_worker_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -102,23 +105,21 @@ func (x *HealthResp) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HealthResp.ProtoReflect.Descriptor instead. -func (*HealthResp) Descriptor() ([]byte, []int) { +// Deprecated: Use StatusInfo.ProtoReflect.Descriptor instead. +func (*StatusInfo) Descriptor() ([]byte, []int) { return file_worker_proto_rawDescGZIP(), []int{0} } -func (x *HealthResp) GetStatus() WorkerStatus { +func (x *StatusInfo) GetStatus() WorkerStatus { if x != nil { return x.Status } - return WorkerStatus_READY + return WorkerStatus_STATE_UNSPECIFIED } type StartResp struct { state protoimpl.MessageState `protogen:"open.v1"` - Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"` - Ip string `protobuf:"bytes,2,opt,name=ip,proto3" json:"ip,omitempty"` - Port int32 `protobuf:"varint,3,opt,name=port,proto3" json:"port,omitempty"` + TestCount int32 `protobuf:"varint,4,opt,name=test_count,json=testCount,proto3" json:"test_count,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -153,23 +154,9 @@ func (*StartResp) Descriptor() ([]byte, []int) { return file_worker_proto_rawDescGZIP(), []int{1} } -func (x *StartResp) GetHost() string { +func (x *StartResp) GetTestCount() int32 { if x != nil { - return x.Host - } - return "" -} - -func (x *StartResp) GetIp() string { - if x != nil { - return x.Ip - } - return "" -} - -func (x *StartResp) GetPort() int32 { - if x != nil { - return x.Port + return x.TestCount } return 0 } @@ -260,25 +247,25 @@ const file_worker_proto_rawDesc = "" + "\n" + "\fworker.proto\"3\n" + "\n" + - "HealthResp\x12%\n" + - "\x06status\x18\x04 \x01(\x0e2\r.WorkerStatusR\x06status\"C\n" + - "\tStartResp\x12\x12\n" + - "\x04host\x18\x01 \x01(\tR\x04host\x12\x0e\n" + - "\x02ip\x18\x02 \x01(\tR\x02ip\x12\x12\n" + - "\x04port\x18\x03 \x01(\x05R\x04port\"*\n" + + "StatusInfo\x12%\n" + + "\x06status\x18\x04 \x01(\x0e2\r.WorkerStatusR\x06status\"*\n" + + "\tStartResp\x12\x1d\n" + + "\n" + + "test_count\x18\x04 \x01(\x05R\ttestCount\"*\n" + "\x10SetTestScriptReq\x12\x16\n" + "\x06script\x18\x01 \x01(\tR\x06script\"\a\n" + - "\x05Empty*.\n" + - "\fWorkerStatus\x12\t\n" + - "\x05READY\x10\x00\x12\b\n" + - "\x04DONE\x10\x01\x12\t\n" + - "\x05ERROR\x10\x022\x92\x01\n" + + "\x05Empty*Z\n" + + "\fWorkerStatus\x12\x15\n" + + "\x11STATE_UNSPECIFIED\x10\x00\x12\x0f\n" + + "\vSTATE_READY\x10\x01\x12\x11\n" + + "\rSTATE_RUNNING\x10\x02\x12\x0f\n" + + "\vSTATE_ERROR\x10\x032\x9f\x01\n" + "\x06Worker\x12,\n" + "\rSetTestScript\x12\x11.SetTestScriptReq\x1a\x06.Empty\"\x00\x12\x1d\n" + - "\x05Start\x12\x06.Empty\x1a\n" + - ".StartResp\"\x00\x12\x18\n" + - "\x04Stop\x12\x06.Empty\x1a\x06.Empty\"\x00\x12!\n" + - "\x06Health\x12\x06.Empty\x1a\v.HealthResp\"\x000\x01B\aZ\x05./genb\x06proto3" + "\x05Start\x12\n" + + ".StartResp\x1a\x06.Empty\"\x00\x12\x18\n" + + "\x04Stop\x12\x06.Empty\x1a\x06.Empty\"\x00\x12.\n" + + "\x13ObserverChangeState\x12\x06.Empty\x1a\v.StatusInfo\"\x000\x01B\aZ\x05./genb\x06proto3" var ( file_worker_proto_rawDescOnce sync.Once @@ -296,21 +283,21 @@ var file_worker_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_worker_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_worker_proto_goTypes = []any{ (WorkerStatus)(0), // 0: WorkerStatus - (*HealthResp)(nil), // 1: HealthResp + (*StatusInfo)(nil), // 1: StatusInfo (*StartResp)(nil), // 2: StartResp (*SetTestScriptReq)(nil), // 3: SetTestScriptReq (*Empty)(nil), // 4: Empty } var file_worker_proto_depIdxs = []int32{ - 0, // 0: HealthResp.status:type_name -> WorkerStatus + 0, // 0: StatusInfo.status:type_name -> WorkerStatus 3, // 1: Worker.SetTestScript:input_type -> SetTestScriptReq - 4, // 2: Worker.Start:input_type -> Empty + 2, // 2: Worker.Start:input_type -> StartResp 4, // 3: Worker.Stop:input_type -> Empty - 4, // 4: Worker.Health:input_type -> Empty + 4, // 4: Worker.ObserverChangeState:input_type -> Empty 4, // 5: Worker.SetTestScript:output_type -> Empty - 2, // 6: Worker.Start:output_type -> StartResp + 4, // 6: Worker.Start:output_type -> Empty 4, // 7: Worker.Stop:output_type -> Empty - 1, // 8: Worker.Health:output_type -> HealthResp + 1, // 8: Worker.ObserverChangeState:output_type -> StatusInfo 5, // [5:9] is the sub-list for method output_type 1, // [1:5] is the sub-list for method input_type 1, // [1:1] is the sub-list for extension type_name diff --git a/worker/proto/gen/worker_grpc.pb.go b/worker/proto/gen/worker_grpc.pb.go index bccdc3b..b5c6e1b 100644 --- a/worker/proto/gen/worker_grpc.pb.go +++ b/worker/proto/gen/worker_grpc.pb.go @@ -19,10 +19,10 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - Worker_SetTestScript_FullMethodName = "/Worker/SetTestScript" - Worker_Start_FullMethodName = "/Worker/Start" - Worker_Stop_FullMethodName = "/Worker/Stop" - Worker_Health_FullMethodName = "/Worker/Health" + Worker_SetTestScript_FullMethodName = "/Worker/SetTestScript" + Worker_Start_FullMethodName = "/Worker/Start" + Worker_Stop_FullMethodName = "/Worker/Stop" + Worker_ObserverChangeState_FullMethodName = "/Worker/ObserverChangeState" ) // WorkerClient is the client API for Worker service. @@ -30,9 +30,9 @@ const ( // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type WorkerClient interface { SetTestScript(ctx context.Context, in *SetTestScriptReq, opts ...grpc.CallOption) (*Empty, error) - Start(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*StartResp, error) + Start(ctx context.Context, in *StartResp, opts ...grpc.CallOption) (*Empty, error) Stop(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) - Health(ctx context.Context, in *Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[HealthResp], error) + ObserverChangeState(ctx context.Context, in *Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StatusInfo], error) } type workerClient struct { @@ -53,9 +53,9 @@ func (c *workerClient) SetTestScript(ctx context.Context, in *SetTestScriptReq, return out, nil } -func (c *workerClient) Start(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*StartResp, error) { +func (c *workerClient) Start(ctx context.Context, in *StartResp, opts ...grpc.CallOption) (*Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(StartResp) + out := new(Empty) err := c.cc.Invoke(ctx, Worker_Start_FullMethodName, in, out, cOpts...) if err != nil { return nil, err @@ -73,13 +73,13 @@ func (c *workerClient) Stop(ctx context.Context, in *Empty, opts ...grpc.CallOpt return out, nil } -func (c *workerClient) Health(ctx context.Context, in *Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[HealthResp], error) { +func (c *workerClient) ObserverChangeState(ctx context.Context, in *Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StatusInfo], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &Worker_ServiceDesc.Streams[0], Worker_Health_FullMethodName, cOpts...) + stream, err := c.cc.NewStream(ctx, &Worker_ServiceDesc.Streams[0], Worker_ObserverChangeState_FullMethodName, cOpts...) if err != nil { return nil, err } - x := &grpc.GenericClientStream[Empty, HealthResp]{ClientStream: stream} + x := &grpc.GenericClientStream[Empty, StatusInfo]{ClientStream: stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } @@ -90,16 +90,16 @@ func (c *workerClient) Health(ctx context.Context, in *Empty, opts ...grpc.CallO } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type Worker_HealthClient = grpc.ServerStreamingClient[HealthResp] +type Worker_ObserverChangeStateClient = grpc.ServerStreamingClient[StatusInfo] // WorkerServer is the server API for Worker service. // All implementations must embed UnimplementedWorkerServer // for forward compatibility. type WorkerServer interface { SetTestScript(context.Context, *SetTestScriptReq) (*Empty, error) - Start(context.Context, *Empty) (*StartResp, error) + Start(context.Context, *StartResp) (*Empty, error) Stop(context.Context, *Empty) (*Empty, error) - Health(*Empty, grpc.ServerStreamingServer[HealthResp]) error + ObserverChangeState(*Empty, grpc.ServerStreamingServer[StatusInfo]) error mustEmbedUnimplementedWorkerServer() } @@ -113,14 +113,14 @@ type UnimplementedWorkerServer struct{} func (UnimplementedWorkerServer) SetTestScript(context.Context, *SetTestScriptReq) (*Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method SetTestScript not implemented") } -func (UnimplementedWorkerServer) Start(context.Context, *Empty) (*StartResp, error) { +func (UnimplementedWorkerServer) Start(context.Context, *StartResp) (*Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method Start not implemented") } func (UnimplementedWorkerServer) Stop(context.Context, *Empty) (*Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method Stop not implemented") } -func (UnimplementedWorkerServer) Health(*Empty, grpc.ServerStreamingServer[HealthResp]) error { - return status.Errorf(codes.Unimplemented, "method Health not implemented") +func (UnimplementedWorkerServer) ObserverChangeState(*Empty, grpc.ServerStreamingServer[StatusInfo]) error { + return status.Errorf(codes.Unimplemented, "method ObserverChangeState not implemented") } func (UnimplementedWorkerServer) mustEmbedUnimplementedWorkerServer() {} func (UnimplementedWorkerServer) testEmbeddedByValue() {} @@ -162,7 +162,7 @@ func _Worker_SetTestScript_Handler(srv interface{}, ctx context.Context, dec fun } func _Worker_Start_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(Empty) + in := new(StartResp) if err := dec(in); err != nil { return nil, err } @@ -174,7 +174,7 @@ func _Worker_Start_Handler(srv interface{}, ctx context.Context, dec func(interf FullMethod: Worker_Start_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(WorkerServer).Start(ctx, req.(*Empty)) + return srv.(WorkerServer).Start(ctx, req.(*StartResp)) } return interceptor(ctx, in, info, handler) } @@ -197,16 +197,16 @@ func _Worker_Stop_Handler(srv interface{}, ctx context.Context, dec func(interfa return interceptor(ctx, in, info, handler) } -func _Worker_Health_Handler(srv interface{}, stream grpc.ServerStream) error { +func _Worker_ObserverChangeState_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(Empty) if err := stream.RecvMsg(m); err != nil { return err } - return srv.(WorkerServer).Health(m, &grpc.GenericServerStream[Empty, HealthResp]{ServerStream: stream}) + return srv.(WorkerServer).ObserverChangeState(m, &grpc.GenericServerStream[Empty, StatusInfo]{ServerStream: stream}) } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type Worker_HealthServer = grpc.ServerStreamingServer[HealthResp] +type Worker_ObserverChangeStateServer = grpc.ServerStreamingServer[StatusInfo] // Worker_ServiceDesc is the grpc.ServiceDesc for Worker service. // It's only intended for direct use with grpc.RegisterService, @@ -230,8 +230,8 @@ var Worker_ServiceDesc = grpc.ServiceDesc{ }, Streams: []grpc.StreamDesc{ { - StreamName: "Health", - Handler: _Worker_Health_Handler, + StreamName: "ObserverChangeState", + Handler: _Worker_ObserverChangeState_Handler, ServerStreams: true, }, }, diff --git a/worker/proto/worker.proto b/worker/proto/worker.proto index 3b16c31..eaf7e66 100644 --- a/worker/proto/worker.proto +++ b/worker/proto/worker.proto @@ -4,19 +4,17 @@ option go_package = "./gen"; service Worker { rpc SetTestScript(SetTestScriptReq) returns(Empty) {} - rpc Start(Empty) returns(StartResp) {} + rpc Start(StartResp) returns(Empty) {} rpc Stop(Empty) returns(Empty) {} - rpc Health(Empty) returns(stream HealthResp) {} + rpc ObserverChangeState(Empty) returns(stream StatusInfo) {} } -message HealthResp { +message StatusInfo { WorkerStatus status = 4; } message StartResp { - string host = 1; - string ip = 2; - int32 port = 3; + int32 test_count = 4; } message SetTestScriptReq { @@ -27,8 +25,9 @@ message Empty { } enum WorkerStatus { - READY = 0; - DONE = 1; - ERROR = 2; + STATE_UNSPECIFIED = 0; + STATE_READY = 1; + STATE_RUNNING = 2; + STATE_ERROR = 3; }