Блог
Пакет объявлял 640 тестов и не выполнил ни одного, а код возврата был 0. Механизм самый обыкновенный, починка — одна строка на импорт, а под ней обнаружилась физическая константа, которую не даёт ни один вход.
Главная строка всего текста состоит из четырёх слов, и напечатал её компилятор:
$ zig test src/root.zig
All 0 tests passed.
В том репозитории 640 тестовых блоков в 87 файлах. Не выполнился ни один. Код возврата — 0, шаг сборки зелёный, и каждый гейт, читавший этот код, пропустил его совершенно правильно.
Zig анализирует объявление верхнего уровня только тогда, когда на него кто-то ссылается. У каждого физического домена в пакете есть корневой файл, и в каждом написано ровно это:
pub const formulas = @import("formulas.zig");
На `formulas` не ссылался никто. Значит, объявление не анализировалось, значит, `formulas.zig` не входил в компиляцию, значит, его тестовых блоков не существовало — не пропущены, не упали, а отсутствуют. То же верно и для всего, что эти файлы импортируют дальше, до самого низа.
Починка — одна ссылка на импорт:
test {
_ = formulas;
}
Понадобилась она в девяти файлах. Измерение сдвинулось с «All 0 tests passed» до 253 прошедших, 1 пропущенного, 254 всего. Тот, что пройти не мог, — предмет последнего раздела.
За эту неделю я обследовал пять пакетов. Ни у одного не было воркфлоу, запускающего `zig build`. Это и есть всё объяснение, и оно скучнее любой альтернативы: ни хитрого бага, ни гонки, ни расхождения версий. Компилятору просто не задали вопрос — он и не дал ответа.
| Пакет | Что остановило | Где остановилось |
|---|---|---|
| zig-half | Шесть дефектов; корень модуля написан синтаксисом Rust | Не компилировался никогда |
| zig-physics | Манифест на JSON, а не на ZON | Строка 1, столбец 7 |
| trinity-training | Зависимость закреплена на 0.2.0, апстрим — 2.1.0 | До компиляции, на загрузке |
| zig-knowledge-graph | Нет сборочного скрипта; зависимость без хеша | Запускать нечего |
| zig-hdc | Путь импорта, уходящий выше корня модуля | Только когда наконец экспортировали |
Пять пакетов, пять непригодных к использованию кем бы то ни было, и причина каждый раз одна и та же. Ошибка манифеста в zig-physics — самая наглядная: `{.name: "zig-physics", version: "0.1.0"}` — это JSON. ZON — не JSON. `zig build` остановился на первой строке и не дошёл ни до одного из 87 файлов за ней.
Последний пакет занял больше всего времени, потому что его отказ был уровнем выше. Файл `vsa_jit.zig` не удавалось экспортировать, а от него зависели пять корней в другом репозитории. Виновником оказался единственный импорт:
const arm64 = @import("../../jit_arm64.zig");
Из `src/vm/` этот путь выходит за корень модуля. Он не может разрешиться — ни у меня, ни в CI, ни где-либо ещё, и ни в один момент истории файла. `jit_arm64.zig` всё это время лежал в той же папке. Починка — удалить пять символов.
Он уцелел потому, что содержащий его файл никто не экспортировал. Вот что стоит унести с собой: **добавление экспорта — это акт верификации.** Оно переводит код из «присутствует» в «досягаем», а досягаемое — единственная категория, которую компилятор проверяет. Экспорт в том PR — долговечная половина правки; починка пути без него сгнила бы точно так же.
Этот файл существует пять раз в трёх репозиториях — дважды в апстриме и по разу у двух потребителей. Я ожидал, что авторитетной окажется та копия, которая компилируется. Она оказалась отставшей.
Отставшая копия вызывала двухаргументный `dotProduct` и читала обычное поле. Копия, которая не компилировалась, вызывала трёхаргументную форму и разворачивала optional — потому что её обновили под новую зависимость, а два нужных ей файла при переносе оставили позади. Она не собиралась нигде — и была новее.
То есть «оно собирается» говорит лишь о том, что копия согласована с зависимостью, лежащей рядом. О том, какая копия должна победить, оно не говорит ничего. Решение по результату сборки протащило бы старый интерфейс в новый код — и сборка по дороге была бы зелёной.
Один тест упал в ту же секунду, как стал досягаем: предсказание параметра Барберо–Иммирци — той связи, что фиксирует спектр площади в петлевой квантовой гравитации. Утверждение — что γ лежит между 0.1 и 0.5, и это верная физика: согласование энтропии чёрных дыр даёт около 0.2375.
Проекция считает γ из двух координат корня E8 как |c₄| + |c₅|, умноженное на φ⁻¹. Но каждый корень E8 — это перестановка (±1, ±1, 0⁶) либо (±½)⁸ с чётным числом минусов, поэтому сумма равна 0, 1 или 2 и ничему больше. Область значений функции — три числа:
| |c₄| + |c₅| | полученная γ | внутри (0.1, 0.5)? |
|---|---|---|
| 0 | 0.436992 — аварийная ветвь | да |
| 1 | 0.618034 | нет |
| 2 | 1.236068 | нет |
Цели 0.2375 среди них нет и быть не может. Единственное допустимое значение — запасная ветвь, срабатывающая, когда обе координаты нулевые: ответ физичен ровно тогда, когда проекции нечего проецировать.
Чтобы это установить, прогон не нужен. Область определения — конечное структурированное множество, поэтому образ вычисляется напрямую и сравнивается с заданным диапазоном; диапазоны не пересекаются, и это доказательство, а не выборка. Приём стоит назвать отдельно, потому что он дешевле тестирования и строго сильнее: если достижимая область значений не пересекает допустимую, не проходит ни один вход.
Я завёл issue и пометил тест пропущенным со ссылкой — не удалил и не расширил границу. Утверждение верное, менять надо проекцию. Написать замену формулы означало бы подогнать её под то утверждение, которому она обязана удовлетворять, — получилась бы модель, согласная с собственным тестом по построению, а именно от этого вся услуга и отказывается.
Трижды за одну сессию локальный тулчейн сообщил об этих репозиториях неправду, и направление стоит назвать точно, потому что только одно из трёх — та ошибка, которой ждут.
Дважды это была ложная тревога: `testing.refAllDeclsRecursive` и `std.time.timestamp` существуют в 0.15.2, на которую эти пакеты нацелены, и удалены позже, — локальный 0.16 сообщал об отказах, которых для цели нет. Один раз — ложное спокойствие в обратную сторону: код, тянущийся к C-аллокатору, проходит на macOS, где libc линкуется всегда, и падает на Linux с «C allocator is only available when linking against libc». Это поймал прогон CI; локально не поймало бы ничто.
Две ложные тревоги и одно ложное спокойствие, от одного прибора, за один вечер. Отсюда следует не «доверяйте CI», а то, что измерение неинтерпретируемо без названного прибора и что отличающийся прибор ошибается в обе стороны, а не консервативно в одну.
Структурный прогон на этом сайте уже отказывается считать пустой результат прохождением: пустой счёт триггеров — отказ, а не ноль. На этой неделе к пакетам, за которыми он теперь следит, добавилась более острая версия того же правила: шаг падает, если выполнилось меньше 200 тестов, — потому что набор, не выполнивший ничего, тоже завершается нулём, и никакой вердикт этих двух случаев не различает.
Такова общая форма. Вердикт — это один бит, и собственную пустоту он в себе не несёт. Везде, где проверка может пройти, не состоявшись, счётчик состоявшегося приходится мерить отдельно и на нём же ставить гейт, — а если вы никогда не видели, как ваш гейт падает, вы ещё не знаете, что он умеет.
Каждая цифра выше измерена, и рядом с ней названы её пределы.