Блог ·
Запуск, що друкував власну невдачу
Перевірочний запуск писав кожен код виходу до підсумку, а тоді все одно писав DONE. Реліз вийшов поверх червоного рядка, якого ніхто не прочитав.
Автор: Provibr
Перед релізом ми запускаємо один скрипт. Він виконує набір тестів вебу, набори пакетів, шлюзу й інструментів, десять перевірок типів, лінт, перевірку змісту, лінт протоколу і породження коду протоколу, і пише по одному рядку на крок у файл підсумку: назву кроку і його код виходу. Потім він пише DONE.
Він писав DONE навіть тоді, коли деякі з тих кодів виходу не були нулем.
Сьомого вересня реліз вийшов, тоді як підсумок містив рядок bun test packages/gateway/tools exit 1. Я прочитав лічильники успіхів і невдач двох наборів і не прочитав третього. Нічого не зламалося: тест, що падав, був сторожею над заголовками вимірювальних скриптів, тож продукту це не торкнулося і клієнт не побачив нічого. Наслідок був добрий, а форма була хибна, і саме форма є єдиною частиною, яку варто записати.
Лічильники не є воротами
Причина, чому це радше соромно, ніж просто не пощастило, полягає в тому, що цей спосіб схибити має назву в наших власних нотатках, і назва ця є вакуумно зеленим: перевірку, не приєднану до рішення, не читають, а перевірку, яку читають очима, читають так, як читає людина, тобто шукаючи те, що очікують побачити.
Підсумок задумували для побіжного перегляду. Кожен набір десь вище друкує власні підсумки, тож природним рухом є подивитися на «4515 pass, 0 fail» і відчути, що все готово. Коди виходу лежали на екран нижче у файлі, останній рядок якого казав DONE.
Полагодка складається з чотирьох слів оболонки. Після всіх кроків скрипт перешукує власний підсумок на будь-який код виходу від одного до девʼяти, і якщо знаходить такий, дописує рядок, що починається з FAIL:. Правило, що йде з цим, полягає в тому, що воротами є DONE без FAIL, а не лічильники. Друга половина цього правила важить більше за першу: людина, яка має дешевий правильний сигнал, перестане будувати дорогі звички навколо ненадійного.
Є супутнє правило, за яким команда розгортання ніколи не ділить ланцюжок оболонки з перевіркою, що лише друкує. Якщо перевірка не може завалити ланцюжок, вона є документацією.
Що було насправді червоним
Сторожа, що впала, належить до цілої родини, і її варто описати, бо вона незвично дешева щодо того, що дає.
Каталог шлюзу тримає вимірювальні скрипти: браузерні прочісування, зонди верстки, засоби засівання та прибирання, ті речі, які раунд пише, щоб щось довести, і потім лишає по собі. Сьогодні їх трохи більше за пʼятсот. Це прості скрипти без перевірки типів, а отже єдине, що стоїть між ними і гниттям, полягає в тому, що хтось випадково котрийсь із них запустить.
Чотири сита проходять по них усіх у звичайному наборі тестів:
- Кожен скрипт розбирається, за допомогою того середовища виконання, яке його справді запускає.
- Кожен відносний імпорт розвʼязується у файл, що існує. Це є тим різновидом мертвоти, якої не бачить розбирач: скрипт синтаксично бездоганний і падає на першому рядку, коли ви його запускаєте.
- Кожен скрипт несе заголовок із прозою всередині. Без пояснення засіб чотири раунди тому не відрізнити від того, який ще має значення.
- Кожен заголовок називає раунд, з якого він походить, в одній із рівно двох сталих форм.
Червоним було четверте. Три скрипти з документаційного раунду мали заголовки, і добрі, але не мали позначки раунду. Це і є весь інцидент: домовленість про найменування, вловлена сторожею, яка існує заради того, щоб пʼятсот скриптів лишалися читними, і яка сиділа в рядку підсумку, що його ніхто не прочитав.
Чому «невідомо» мусить коштувати хеш коміту
Четверте сито приймає дві форми і нічого більше. Або заголовок каже назву раунду з датою і те, звідки це приписування взялося, або він каже, що раунд невідомий, і несе хеш першого коміту файлу разом із датою.
Назві прямо заборонено бути словом «невідомо». Без цього винятку «раунд невідомий, 2026-09-04» є цілком чинною назвою з датою, і дорогої гілки можна уникнути одним словом. Як воно є, визнання того, що ви не знаєте, коштує вам хеша, який комусь доведеться піти і знайти, а поки він його знайде, він зазвичай уже знає раунд.
Дата є обовʼязковою з іншої причини. Гола назва є прозою, а не позначкою: речення на кшталт «раунд, який також торкався роботи над заголовками» містить назву раунду і не стверджує нічого про файл, у якому воно стоїть. З прикріпленою датою воно стає твердженням про цей скрипт, зробленим певного дня, а це найменше, за що можна когось тримати.
Ворота мусять стояти на нулі
Очевидним питанням щодо пʼятисот нетипізованих скриптів є те, чому ми не перевіряємо їхні типи. Ми це виміряли, і відповідь повчальна.
Налаштування існує. Воно є приладом, а не воротами, і у перевірочному скрипті не запускається. Виміряно по скриптах у тому стані, у якому вони були:
| Налаштування | Помилки |
|---|---|
| Зі строгими налаштуваннями цієї робочої області | 3180 |
| Без строгості | 398 |
| Без строгості, після того як один спільний файл зробили типізовним | 159 |
Ворота мусять стояти на нулі, а ті 159, що лишилися, здебільшого не є дефектами. Дві родини обійшли, і обидві виявилися артефактами типізації нетипізованого коду: параметр без початкового значення, який випадає з виведеної форми, і цикл із деструктуруванням по мішаному масиву, що розширюється до обʼєднання. Двадцять сім із них є однією й тією самою діагностикою, про імпорт файлу TypeScript зі скрипта, що працює під завантажувачем, який чудово з ним дає раду.
Що прилад справді дав, так це крок від 398 до 159. Один рядок у спільному браузерному помічнику, конструктор, що гуртом привласнює свої аргументи примірникові, тримав живими 239 помилок, бо кожне звертання до властивості цього обʼєкта в кожному скрипті було нерозвʼязним. Одна полагодка, і 239 помилок зникло. Це добрий зиск для вимірювання, якому дозволено бути ненульовим, і саме такого зиску ви не дістаєте від воріт, бо ворота, які неможливо задовольнити, вимикають.
Перевірка, що виміряла власне середовище виконання
Щоб перевірка розбору запрацювала, знадобилося дві спроби, і обидві схибили у спосіб, що виглядав як проблеми продукту.
Перша версія просила поточне середовище виконання перевірити кожен файл. Під нашим запускачем тестів поточне середовище не є тим середовищем, під яким працюють скрипти, а його прапорець перевірки означає геть інше. Наслідком стало 301 повідомлення про синтаксично зламані файли, де рядки джерела наводилися як текст помилки. Кожен із них був справний.
Друга версія називала правильне середовище явно і все одно схибила, тільки інакше: запускач тестів жне дочірні процеси, що належать тестові, і приблизно з половини переліку кожен виклик повертався зі станом null. Убитий сигналом, а не помилкою розбору. Сторожа, що червоніє випадково, сторожею не є.
Версія, що працює, породжує один дочірній процес, який сам перевіряє файли по вісім за раз, приблизно за девʼять десятих секунди, коли це виміряли. Те, що стверджується, не змінилося: чи прийме ці скрипти те середовище виконання, яке їх запускає.
Паралельність теж не є прикрасою. Запущена послідовно, перевірка перевищувала часову межу на тест, а сторожа, що вичерпує час, читається як зламана, а не як повільна.
Нижня межа, якої ніхто не просить
У тому файлі є пʼятий тест, який стверджує лише те, що скриптів для перевірки більше за пʼятдесят.
Обхідник каталогів, що не знаходить нічого, робить кожне твердження після себе тривіально правдивим. Перейменуйте каталог, змініть фільтр розширень, зламайте обхідник, і чотири зелені сита звітуватимуть про порожній перелік. Нижню межу навмисно поставлено значно нижче за справжнє число, бо річ не в тому, щоб завадити прибиранню; річ у тому, щоб зробити видимою різницю між «нічого не негаразд» і «нічого не оглянуто».
Той самий файл тесту чесний щодо своїх меж, і ця чесність належить до задуму. Жодне з чотирьох сит не доводить, що скрипт усе ще міряє те, про що каже. Селектор, якого вже немає, маршрут, що переїхав, стовпець, який прибрали: усі чотири перевірки проходять. Це підлога, а не перевірка типів, і сказати про це у файлі означає завадити наступній людині вважати зелений запуск чимось більшим, ніж він є.
Що ми б записали
- Перевірка, що лише друкує, не є воротами. Вона мусить уміти завалити те, що йде після неї.
- Пишіть рядок про невдачу, а не самі коди виходу. Воротами є слово, яке хтось може знайти через grep, на сталому місці, а не число, яке треба помітити.
- Ніколи не чіпляйте розгортання за перевіркою, що не може його спинити. Ланцюжок і є обіцянкою.
- Лічильники є підсумком, а не вироком. Люди читають те число, яке очікують побачити.
- Ворота мусять уміти стояти на нулі. Якщо вимірювання цього не може, тримайте його як прилад і скажіть про це, інакше його вимкнуть, а потім забудуть.
- Зробіть дешеву втечу від правила дорогою. Якщо визнання незнання коштує одне слово, це слово пишуть усі.
- Перевірка, що падає випадково, не є перевіркою. Женіться за хиткістю раніше, ніж повірите сигналові.
- Стверджуйте нижню межу того, що знайшов обхідник. Чотири зелені перевірки над порожнім переліком виглядають точно як чотири зелені перевірки.
Для ясності про те, чим це є: рядок про невдачу стоїть у скрипті, що лежить у сховищі, і відтоді пройшов над зливаннями, які були зеленими. Червоним запуском у справжньому вжитку його ще не доведено, лише тим підсумком, проти якого його написали. Цифри типізації походять з одного вимірювання одного дня, а лічба файлів рухається щоночі.
Із платформної роботи над Provibr. Іншу сторожу, якій довелося довести власний нуль, описано в виняток доступності, який себе вимірює, а поле, що тихо тримало намір замість факту, описано в майбутня дата не є розкладом виходу.