Стендап Сьогодні 📢 Канал в Telegram @stendap_sogodni
🤖🚫 AI-free content. This post is 100% written by a human, as is everything on my blog. Enjoy!Пости з тегом #Go
05.11.2024
Теги в каналі та HTML
Не так все просто з тегами, як я вчора розповідав. А саме, замінити тег всередині тексту не можна, якщо він вже є посиланням. Ну, наприклад, якщо я вирішив послатися на сторінку Вікіпедії про Go, а також й додав тег Go, то тегом повинна бути друга згадка, а не перша, яка в посиланні.
Іншою особливістю тут є те, що метадані (разом з тегами) є частиною вихідного тексту, а значить, зʼявляються не до, а одночасно з обробкою Markdown. Тому легше за все замінити теги вже в підготованому для Telegram тексті. Який є потворним гібридом HTML та текстової розмітки. Тож доведеться заміняти в HTML.
Як заміняти? Точно не регулярними виразами. Тут потрібний той чи інший парсер HTML. В Go для того є модуль net/html - я про нього вже згадував. Він надає, окрім іншого, токенізатор HTML, тобто спосіб перетворити HTML в послідовність токенів: “тег”, “текст”, “коментар” тощо.
Для такої задачі токенізатор краще, ніж розбір документа в дерево, оскільки дерево нас не цікавить. Нам важливо тільки одне: чи знаходиться текст всередині посилання. Тому заводимо прапорець “чи ми в посиланні” та біжимо по документу.Бачимо токен “відкрити тег A” - вмикаємо прапорець; “закрити тег A” - вимикаємо прапорець. Якщо зустріли токен “текст” та прапорець вимкнений — шукаємо в ньому теги. А решту токенів копіюємо в вихід, як є.
Замість складного обходу дерева чи пошуку батьків — простий прохід з тривіальним скінченним автоматом. Швидко та прозоро.
08.11.2024
Пошук перегонів даних в Go
🏁 Перегони даних — це коли два рівночасні процеси наввипередки оновлюють змінну, без відома про існування один одного (Мені не дуже зрозуміло, чому це називається перегонами.) В результаті отримуємо напівзіпсоване значення, в якому не врахована частина обчислень. Причому, звісно, рівночасність — непередбачуване явище, тому псування трапляється не кожного разу, а може взагалі чекати на високе навантаження в продакшні.
Одним словом. Шукав, чому випадково хибиться тест. Звузив причину до ділянки з використанням go-redis#ClusterClient.ForEachMaster - вона збирала значення з усіх серверів Redis в один масив. Спочатку думав, що тесту бракує очікування (ще одна класична причина випадкових хиб) та намагався виправити. Тут є легка перевірка, насправді: додай свідомо перебільшене очікування — може, 10 секунд — якщо це не виправляє тест, то проблема в іншому.
А потім помітив в документації по ForEachMaster, що вона-то рівночасна!. Звісно, через рівночасність зміст вихідного масиву був неповним, до того ж для кожного хибного випадку різний. Щоб виправити, я додав збір результатів через канал, але можна було б й мьютекс поставити. Такі API з прихованою рівночасністю мене обурюють, бо з ними хоч знаєш, а не зробиш правильно.
Тут я згадав, що в Go є детектор перегонів та раптом дізнався, що він не увімкнений за замовчуванням. Увімкнув — та дійсно, проблемний тест відразу “засвітився”. Хотів вже увімкнути для всіх тестів та локальних запусків, але виявив пару проблем з цим. По-перше, тести з детектором тривають в три рази більше. По-друге, як не дивно, але для компіляції з детектором потрібно увімкнути CGO, а це йде зі своїм пакетом ускладнень, як-от неможливість кроскомпілювати з macOS в Linux. Довелося відмовитись.
Нарешті, детектор знайшов ще одну гідну обурення ситуацію. Такого я ще не бачив. Офіційних серіалізатор Protobuf у JSON - protojson
- додає у випадкові місця JSON пробіли. Та цього навіть не можна вимкнути!. Пояснюють вони це тим, що “споживачі мусять не очікувати чіткого формату”, оскільки команда Protobuf з ним ще не визначилася (sic!) Тому поки відстежуємо обговорення, якому вже 4 роки, а в тесті довелося декодувати JSON та порівнювати не рядком, а за змістом.
21.11.2024
Розширена інформація після невдалого тесту в Go
Захотілося, щоб після невдалого інтеграційного тесту я бачив журнал сервісів з Docker Compose. Бо в мене на CI вже й так журнал друкується наприкінці збірки, але за тим загальним журналом складно знайти потрібне місце.
Перше питання стало — як взагалі в Go викликати код після тесту? Рішення в стандартній бібліотеці не знайшов, але ті тести виконуються пакетом testify/suite
, а в нього є метод TearDownTest(), проміж інших. Тож тут проблем немає.
…Далі, як дізнатися, що тест був невдалий? Я очікував якогось аргументу в колбеці, але ні. Насправді той обʼєкт *testing.T
, навколо якого обертаються тести, має метод t.Failed(). (Взагалі раджу його вивчити детальніше, бо там багато чого цікавого, наприклад, можливість отримати тимчасову директорію для одного тесту - t.TempDir()
.) А в testify/suite
, до речі, через s.T()
отримуємо той самий обʼєкт.
(PS: а ще в testify/suite
знайшов suite.HandleStats - якщо його оголосити, то цей метод буде викликаний наприкінці всього пакету зі статистикою про час виконання та результати тестів.)
Окей, тепер, як отримати логи? Спробував через програмний клієнт Docker… надто низькорівневий, бо він повертає для логи конкретного контейнера, та ще й потоком. А мені потрібні зведені логи всіх контейнерів в проєкті. Тому просто викликаю консольну команду docker compose
. Для того є зручний високорівневий тип exec.Cmd. Йому можна прямо сказати “спрямуй вивід команди в мій вивід” і все, готово.
14.01.2025
Тести з базою на Go
Як я два роки тому писав, так і досі користуюся бібліотекою testfixtures для підготовки тестової бази. Фікстура — це такий файл (шаблонізованого) YAML, в якому сидить зміст однієї таблиці.
В мене специфіка проєкту така, що є багато складних запитів, тож фікстур доводиться створити чимало. Мені цей підхід дуже не подобається, бо фікстури мають по файлу на кожну таблицю, та логічні звʼязки між записами не проглядаються. Та друга проблема фікстур — немає можливості прибрати обовʼязкові в схемі бази, але несуттєві для тесту значення.
Одним словом, я все більше дивлюся, як уникати бази в тестах взагалі. Для когось це, може, очевидний підхід, але я звик до Rails та factory_bot, де наочно та стисло можна створювати довільні ієрархії обʼєктів. Втім, factory_bot
спирається на ActiveRecord, а для Go в мене нічого схожого немає.
Базу можна замокати — для того є бібліотеки sqlmock чи pgxmock. Вони дають можливість задати результат для кожного запиту. Що приємно, не обовʼязково писати весь запит, достатньо підрядка, наприклад, з використанням sqlc вистачить назви запиту.
З моками бази зручно, що запити будь-якої складності заміщаються легко: достатньо вказати, які рядки він має повертати. Мені тільки не подобається, що моки нічого не знають про структуру бази, тому легко помилитися зі змістом або забути оновити моки після міграцій. В результаті будемо тестувати із хибним прикладом та не знати краще.
Тому мені ще більше подобається підхід вищого рівня. Оскільки код, що виконує запити, ми генеруємо з sqlc, тобто його тестувати не потрібно, то можна мокати згенерований інтерфейс. Він повертає структури, цілісність яких гарантує (в міру можливого) sqlc, а для моків можна використати хоч mockery, хоч будь-яке інше рішення, бо до бази вони вже не мають відношення.
Для повного комплекту гарно ще вкрити тестами модуль sqlc
, щоб переконатися що самі запити працюють вірно.