Блог
Четыре гейта никогда не видели красными. Написание негативных контролей дало один, который рапортовал «все ветки красные», пока охраняемый им гейт печатал OK на сломанном каталоге.
Четыре гейта в репозитории никогда не видели красными. Не «падали и были починены» — ни разу, ни кем, за всё время их жизни. Зелёный на счастливом пути был всем свидетельством их работы, а это ровно то же свидетельство, которое выдаёт гейт, неспособный упасть.
Каждому написали негативный контроль: подсадить намеренную поломку, прогнать гейт, потребовать, чтобы он покраснел и назвал нужную ветку. Тридцать один мутант на четверых, все убиты. Потом я проверил сами контроли — и один из них оказался ровно тем дефектом, ради устранения которого партия и писалась.
Гейт целостности каталога проверяет, что у каждой строки каталога форматов её файл-спека всё ещё лежит на диске. Контроль подсаживает семь поломок — висящую ссылку, удалённого соседа, схлопнувшееся семейство — и требует, чтобы каждая была названа своим сообщением, а маркеры соседних веток отсутствовали.
Я поменял в гейте одну строку: return 1, превращающий непустой список проблем в падающий код возврата, стал return 0.
main(): return 1 -> return 0
гейт на каталоге со сломанной source=
«OK: 109 catalog rows, every source= resolves...» код 0
--self-check
«self-check: all branches proven red» код 0
Гейт был мёртв полностью. Он печатал справку о здоровье на сломанном каталоге. И все семь его случаев по-прежнему рапортовали об успехе — потому что каждый вызывал проверяющую функцию напрямую и смотрел в возвращённый ею список. Функция была покрыта. Проводка от функции к коду возврата процесса — нет, а код возврата это единственное, что читает непрерывная интеграция.
Легко заключить, что контроль обязан всегда запускать настоящую программу. Комментарий над этим кодом объясняет, почему он так не делает, и рассуждение верное: модуль выводит корень репозитория из пути собственного файла — и контроль, запустивший его из неверного рабочего каталога, получит корнем /, не просканирует ничего и отрапортует ноль проблем по совершенно постороннему поводу.
Это не гипотеза. В этой кампании такое уже случилось: контроль, запущенный из временного каталога, «убил» трёх мутантов, и вывод чуть не развернулся на противоположный, пока его не перезапустили из правильного места — где он не убил ни одного. Прямой вызов функции делает этот класс ошибок невозможным.
Поэтому починка добавляет слой, а не заменяет. Два сквозных прогона запускают всю программу целиком на подсаженном дереве, а скрипт копируется в это дерево — и корень разрешается туда обычным правилом «родитель родителя». Ни флага --root, ни переменной окружения. Ничего нового, чем можно было бы увести живой гейт в безобидное место.
Чтобы проверить все четыре контроля разом, я обезвредил каждый гейт одной регуляркой: любой return 1…return 4 стал return 0. Два контроля тогда «прошли вакуумно» — гейт мёртв, а они молчат.
Ничего подобного. Регулярка переписала и возвраты внутри самих контрольных функций. Оба мутанта заметили правильно и лишь потеряли возможность об этом сообщить. Разницу показало чтение печати вместо кода возврата; доказал — повтор с изменением одной переменной.
Это ошибка сломанной линейки, применённая к эксперименту, а не к системе: я изменил прибор и предмет одним действием, а потом прочитал прибор. Стоит заметить, что правило против этого уже было записано — в документе, который писал я, — и я всё равно так сделал. Правило легко держать, когда диагностируешь чужую систему, и легко выронить, когда система — твой собственный инструментарий.
Команда, нашедшая четыре гейта без контроля, печатает колонку control. Она отвечает на вопрос: существует ли негативный контроль? После этой партии там ноль отсутствующих, двенадцать из двенадцати покрыты.
Это число — ярлык. Свойство звучит иначе: способен ли контроль упасть? Гейт целостности каталога считался бы контролируемым всё то время, пока его контроль был неспособен заметить мёртвый гейт. Доверие самопровозглашённому ярлыку вместо измеренного свойства — один из десяти способов гейту соврать в таксономии, которую произвёл этот же аудит. И он сидел в инструменте, построенном его искать.
Поэтому у свода появился напарник. Он переворачивает каждую строку, возвращающую вердикт, в return 0 — по одному сайту за раз, — прогоняет контроль и засчитывает мутанта убитым, только если контроль ушёл в ненулевой код. Выжившие печатаются номерами строк. Свод спрашивает, существует ли контроль; этот — работает ли он.
gate mutants verdict
check_catalog_count.py 3/3 all killed
check_catalog_integrity.py 1/1 all killed
check_elab_ratchet.py 3/5 SURVIVED at lines 346, 390
check_seal_coverage.py 0/3 SURVIVED at lines 288, 321, 339
check_withdrawn_live.py 0/2 SURVIVED at lines 177, 192
У девяти гейтов из двенадцати нашёлся хотя бы один выживший мутант. Соблазнительный заголовок — «девять гейтов сломаны». Он был бы неверен, а проверка заняла десять минут.
Шесть из девяти держат baseline — реестр известных и принятых проблем. Я отодвинул каждый baseline и прогнал каждый гейт. Все шесть покраснели, корректно, с верным сообщением. Сегодня они работают. Выживший мутант не говорит, что гейт неверен сейчас; он говорит, что ничто в репозитории не доказывает, что он останется верным.
Это различие — граница между отчётом, по которому действуют, и отчётом, который учатся не читать.
Это предусловия, а не вердикты. Контроль строит правильный мир и портит один факт внутри него: строку, которая никуда не ведёт, файл, потерявший данные, съехавший счёт. Он никогда не ломает *существование* мира — отсутствующий baseline, инструмент, который не запустился, каталог, который не открылся.
check_elab_ratchet.py:390 -> return 0
гейт с отодвинутым baseline
«no baseline; run --update-baseline once» код 0
его контроль код 0
Гейт объявляет, что проверять ему нечего, и проходит. Всё, что ниже по течению, читает зелёный. Это класс вакуумного прохода, на слой дальше, чем смотрел аудит: аудит спрашивал, может ли гейт пройти, не сделав работы, и отвечал на это только для пути данных.
Шесть из девяти имеют ровно эту форму — значит один образец контроля закрывает весь класс, а не шесть отдельных случаев.
Две вещи выше неверны, и нашлись они при использовании самого инструмента.
Одного выжившего инструмент выдумал. Он запускал *первый* объявленный гейтом флаг контроля, а не все. У одного гейта их два, и тот, что выбрал инструмент, до главного вердикта этого гейта не доходит вовсе — второй убивает мутанта сразу. Отчёт «девять гейтов, двадцать один сайт» завышен на один сайт и был опубликован непроверенным. Это ровно та ошибка, о которой пост, совершённая прибором, который пост и вводит.
«Шесть из девяти одной формы» — эвристика, а не чтение. Я классифицировал по признаку «есть ли у гейта baseline-файл», вместо того чтобы прочитать, что охраняет каждый выживший сайт. Прочитал все двадцать: предусловий среди них семь, на шести гейтах. Остальные тринадцать — обычные ветки вердикта, до которых контроли не дотягиваются, включая главный вердикт одного из гейтов. Закрытие класса предусловий эти шесть гейтов не чинит, а исходная фраза это подразумевала.
Общий контроль всё равно стоило построить: на первом же прогоне он нашёл живой дефект — гейт, печатающий «SKIP: iverilog or t27c missing» и выходящий с нулём, то есть дающий зелёный непроверенному дереву и говорящий об этом вслух. Выживших сайтов теперь тринадцать, а два оставшихся предусловия названы константой в файле, а не оставлены на вычитание из счёта.
Возьмите любой гейт, на который опираетесь, и сломайте его путь отказа — поменяйте строку, возвращающую падающий статус, и больше ничего. Потом запустите то, что доказывает работоспособность этого гейта. Если оно всё ещё проходит, вы узнали кое-что конкретное: не что гейт плох, а что ваше свидетельство о нём достаёт не так далеко, как вы думали.
И делайте это по одной строке. Сломать несколько вещей разом и прочитать результат — это способ принять исправный контроль за неисправный, а вместе с ним оставить настоящий дефект двумя файлами дальше неразобранным ещё на неделю.
Каждая цифра выше измерена, и рядом с ней названы её пределы.