Известный инженер и сооснователь компании HashiCorp Митчелл Хашимото выступил на техническом мероприятии компании Antithesis с докладом, посвященным преодолению барьеров в автоматизированном тестировании. На примере своих проектов — масштабных сетевых утилит HashiCorp и современного эмулятора терминала Ghosty — спикер продемонстрировал, как неочевидные паттерны проектирования позволяют тестировать графические процессоры, глубокие побочные эффекты и хаотичные конфигурации операционных систем. Главная идея выступления заключается в том, что практически любой «нетестируемый» код можно покрыть надежными проверками, если своевременно переработать архитектуру самого программного обеспечения.
🛠 Разрушая миф о «нетестируемом» коде 0:00
В практике разработки регулярно возникает ситуация, когда при открытии нового пул-реквеста автор заявляет, что написанный им код невозможно протестировать автоматически, добавляя, что он «проверил всё вручную». По словам Митчелла Хашимото, в реальности фраза «это нельзя протестировать» почти всегда означает лишь то, что это нельзя сделать легко или прямо сейчас. Инженер подчеркивает, что выбор, стоит ли тратить ресурсы на написание сложного теста, всегда остается за разработчиком, однако техническая возможность существует практически всегда.
Спикер разделяет весь процесс обеспечения качества на два одинаково важных направления, которые визуализированы в его презентации специальными пиктограммами:
- Стратегия тестирования (иконка пробирки): непосредственно методология и подходы к тому, как именно проводить проверки.
- Тестируемость (иконка мыльных пузырей): свойство самой архитектуры кода, позволяющее автоматизировать эти проверки.
По мнению Хашимото, разработчики слишком часто игнорируют именно тестируемость. Если архитектура приложения изначально не дружелюбна к автоматизации, не предоставляет нужных API и не имеет четкой структуры, то утверждение о невозможности тестирования становится горькой правдой. В своем докладе, который стал идейным продолжением его знаменитого выступления на GopherCon 2017 года, спикер представил эволюцию инженерных подходов от простых концепций к продвинутым.
📸 Снапшот-тестирование: магия контекстных диффов 8:22
Первой важной стратегией в арсенале инженера выступает снапшот-тестирование, известное также как тестирование по эталонным файлам (golden files) или поиск «абсолютной истины» (ground truth). Этот подход незаменим, когда программа генерирует сложный выходной формат, прямое попиксельное или построчное сравнение которого в коде превращается в кошмар. Однако Хашимото выделяет еще одно, более редкое преимущество снапшотов — они предоставляют разработчику гораздо более информативные разности (diffs) при сбоях.
В качестве примера спикер привел разработку своего текущего проекта Ghosty — кроссплатформенного эмулятора терминала с открытым исходным кодом, написанного на языке Zig. Программа содержит встроенный шрифт (sprite font) для рендеринга глифов (например, стрелок в стиле PowerShell), которые должны идеально вписываться в сетку терминала. В процессе разработки команда столкнулась с багом: при нечетных размерах сетки возникала ошибка смещения на один пиксель.
Для решения этой проблемы было внедрено снапшот-тестирование:
- Программа программно растеризует каждый глиф при заданных размерах сетки и сохраняет эталонный битмап прямо в репозиторий.
- При запуске тестов актуальный рендеринг побайтово сравнивается с эталоном.
- В случае несовпадения тест автоматически генерирует изображение-разность, подсвечивая проблемные пиксели зеленым цветом.
Хашимото отмечает, что стандартные юнит-тесты в таких случаях выдали бы абстрактную ошибку в духе «пиксель X не совпал», лишенную контекста. Визуальный же дифф сразу показал, что проблема затрагивает вертикальные линии во множестве глифов, что мгновенно навело инженера на мысль об ошибке в общей функции отрисовки.
Подобный подход спикер успешно применял еще десять лет назад при работе над Terraform. На первом этапе Terraform строит граф ресурсов для выполнения. Проверять связи в графе обычными утверждениями (assertions) было тяжело, и инженеры тратили часы на поиск изменений. Хашимото изменил подход: система начала экспортировать ожидаемый и полученный графы в формат DOT, где отсутствующие связи окрашивались в красный цвет, а лишние — в зеленый, что драматически ускорило отладку.
🧼 Изоляция побочных эффектов: архитектура Read-Process-Write 13:47
Главным «множителем силы» для создания тестируемого кода Хашимото считает изоляцию побочных эффектов. Типичная дилемма разработчика: функция содержит полезную логику, но намертво завязана на внешнее сетевое или дисковое IO. Задача инженера — обнаружить внутри этого «супа со спецэффектами» чистую функциональную логику, извлечь её наружу и перестроить порядок выполнения.
На выходе получается чистый тест, работающий по принципу «мусор на входе — мусор на выходе» (garbage in, garbage out). Это означает, что разработчик сам искусственно передает входные данные вместо реального окружения, поэтому они должны быть строго реалистичными.
Анатомия эволюции этого паттерна наглядно видна на примере обработчика клавиатурного ввода в Ghosty:
- Ранняя архитектура: при нажатии клавиши функция последовательно опрашивала состояние мыши, проверяла состояние клавиатуры (модификаторы, повторы), считывала внутренние настройки терминала (схемы кодирования Kitty или legacy) и лишь затем кодировала символ и отправляла его в PTY. Код был полностью нетестируемым из-за сложности эмуляции состояний железа.
- Проблема регрессий: спикер признается, что исправление бага для одной раскладки ломало ввод для пользователей других, экзотических для него языков (русского, японского, китайского, венгерского). Например, симулировать настройки протокола Kitty совместно с русской раскладкой вручную было крайне сложно.
- Рефакторинг: Хашимото потратил несколько часов, чтобы разглядеть скрытую структуру кода, и переписал его в концепции Read-Process-Write. Весь сбор внешних данных (около 15 полей от ОС и настроек) был вынесен на самый верх функции. Сама логика кодирования превратилась в чистую функцию, которая принимает сухие параметры и возвращает результат.
Благодаря такой изоляции, сложнейший код кодирования стал легко поддаваться юнит-тестированию и фаззингу. По мнению Хашимото, именно этот шаг позволил Ghosty получить одну из самых стабильных и полных реализаций обработки клавиатурного ввода на рынке.
🎮 Как протестировать GPU, если документация молчит 21:12
Развивая тему изоляции, спикер перешел к одной из самых закрытых областей — программированию графических процессоров (GPU). Ghosty использует ресурсы видеокарты для отрисовки интерфейса терминала и некоторых сопутствующих вычислений. Когда Хашимото впервые задался вопросом, как тестировать шейдеры и логику рендеринга, он столкнулся с полным отсутствием информации в индустрии. Первая страница поисковой выдачи Google состояла из бесполезного мусора.
Тогда инженер скачал официальные спецификации DirectX, Direct 3D, Metal, OpenGL и Vulkan от Khronos Group, Microsoft и Apple. Общий объем изученных PDF-документов составил около 4000 страниц. Хашимото провел контекстный поиск по словам «test», «verify», «snapshot», «bug», «stability» и не обнаружил ни одного совпадения. В официальных руководствах трех крупнейших вендоров вообще не упоминалось, как тестировать код для GPU.
Для решения этой задачи Хашимото разделил архитектуру рендеринга на две изолированные составляющие, учитывая особенности работы с видеокартами:
- CPU-сторона (подготовка данных): центральный процессор формирует описание задач — граф операций (вершины и ребра) и вложения данных (data attachments), которые по сути являются буферами байт.
- GPU-сторона (исполнение): видеокарта выступает как чистый вычислитель функций, не имеющий доступа к диску, сети или периферии, а оперирующий только своей памятью.
Чтобы покрыть тестами CPU-часть (а это 13 000 из 15 000 строк кода рендерера Ghosty), Хашимото изолировал API-вызовы видеокарты. Функция принимает состояние сцены, генерирует граф задач и байтовые буферы, структура которых проверяется снапшот-тестами. Нетестируемым остался лишь тонкий слой перевода готового графа в нативные команды драйвера, который практически никогда не меняется.
Для тестирования GPU-стороны Хашимото применил метод полных проходов рендеринга (full render passes) с записью в буферы, доступные для чтения CPU. Тест искусственно создает миниатюрный терминал размером 2x2 пикселя, отправляет задачу на GPU, минуя вывод на физический экран, считывает получившийся фрейм из памяти и побайтово сравнивает картинки. Такой тест работает мгновенно и исключает проблемы стандартных скриншот-тестов (тайминги, рамки окон, позиции).
В качестве перспективных экспериментов для изоляции отдельных шейдеров спикер упомянул использование механизма Transform Feedback в OpenGL, позволяющего сбрасывать строковые переменные шейдеров напрямую в буфер CPU. Поскольку в современных API (Vulkan, Metal) эта функция отсутствует, для Metal Хашимото разработал концепт с вынесением логики в вычислительные шейдеры (compute shaders), хотя это и требует избыточного изменения структуры кода GPU.
❄️ Виртуализация через Nix: укрощение хаоса Linux-десктопа 34:39
Существуют компоненты системы, которые невозможно протестировать без полноценной симуляции внешнего мира — движений мыши, нажатий физических клавиш, сетевых сбоев или отказов диска. Для этих целей Хашимото задействует тестирование в виртуальных машинах через экосистему Nix. Признавая, что повальное увлечение Nix в индустрии порой утомляет разработчиков, спикер заявляет, что за свою 15-летнюю карьеру в сфере виртуализации и контейнеризации он не встретил более эффективного инструмента для этих задач.
Фреймворк тестирования Nix обладает тремя критически важными свойствами:
- Официальный статус (First-party): проект Nix использует этот фреймворк для тестирования самого себя, что гарантирует его долгосрочную поддержку.
- Полноценная тестовая среда: это не просто запуск контейнера (как в Docker), а готовый API для написания проверок и утверждений.
- Доступ к пакетной базе Nix за десятилетия: позволяет жестко зафиксировать версии ядра, системных библиотек (glibc) и любого ПО.
Хашимото иронично отмечает, что это единственный способ отлаживать баги от пользователей дистрибутива Debian, которые приходят с древними версиями софта с долгосрочной поддержкой (LTS) и жалуются на некорректную работу приложения.
Процесс тестирования состоит из трех шагов: сначала в конфигурации NixOS описывается виртуальная машина со всеми драйверами, пользователями и ядрами; затем на языке Python пишется сценарий теста (фреймворк поддерживает даже распознавание текста на экране через OCR); после чего встроенный рантайм запускает тесты с возможностью интерактивной отладки через REPL.
Для Ghosty виртуальные машины жизненно необходимы при тестировании систем ввода (Input Methods), таких как IBus или FCITX, используемых для азиатских языков и эмодзи-клавиатур. По словам Хашимото, специфика Linux заключается в том, что фреймворк ввода, оконный менеджер и композитор пишутся разными командами, из-за чего их комбинации ведут себя непредсказуемо. В отличие от экосистемы Apple, где действует вертикальный мандат на жесткую интеграцию компонентов, разработчикам под Linux приходится вручную укрощать этот хаоc. Также через VM тестируются интеграции с рабочим столом, например появление пункта «Открыть в Ghosty» при правом клике мыши, что регулярно ломается при обновлениях графических сред.