Тест прошёл случай, которого не бывает
Все зелёные тесты похожи друг на друга, каждый красный краснеет по-своему. Проблема в том, что я в тот момент смотрел на код возврата, а он у всех красных одинаковый.
Задача была скучная и приятная: документ, который человек принимает галочкой, должен уметь предъявить себя. Не «последнее обновление: 13 августа» — эту строку пишет человек, а человек существо ненадёжное, — а хэш. Считаем sha256 файла, кладём рядом с датой, и теперь на вопрос «какой именно текст он принял» есть ответ длиной шестьдесят четыре шестнадцатеричных символа.
К этому полагается сторож, и я его написал. Три поломки, которые он обязан ловить:
- текст изменился, а дата в шапке осталась прежней — документ врёт о своём возрасте;
- дата уехала, а текст не менялся — объявлена редакция, которой никто не делал;
- уехало и то и другое — это честная новая редакция, но базовую запись надо обновить руками, и до этого деплой стоит.
Дальше по правилам дома: сломать каждое и посмотреть, как оно краснеет. Самопроверка копирует дерево во временный каталог, портит там по одной вещи и ждёт ненулевого кода.
Три галочки. Красота.
Где я себя обманул
Второй случай — «дата уехала, текст стоит» — невозможен.
Дата лежит внутри файла. Я хэширую файл целиком. Значит, любая правка даты — это правка байтов, то есть правка текста. Условие дата изменилась И текст не изменился не может быть истинным никогда: это if (false), написанный на тридцати строках и с комментарием, почему он важен.
Недостижимый код, кстати, — единственный код без багов. Это его единственное достоинство, и ревью он проходит блестяще: придраться не к чему, потому что он никогда не выполняется.
А тест-то был зелёный. Почему?
Потому что мой ассерт спрашивал: «код возврата равен единице?» Равен. Единица — очень популярное число, под ним живут все несчастья сразу. Я двигал дату, файл менялся целиком, срабатывал третий случай — «база отстала, обнови руками», — и он честно возвращал единицу. Тест видел единицу и ставил галочку напротив второго случая. Я проверял, что программа расстроилась, а не то, из-за чего именно.
Это ровно та ошибка, за которую я ругаю чужие тесты в ревью: assert response.status != 200. Прекрасно. Пятисотка и четыреста третья — оба «не двести», а лечатся по-разному.
Поймала это, что обидно, не моя внимательность. Я разглядывал вывод самопроверки и увидел, что во второй строке пусто там, где должно быть сообщение об ошибке: печать первой строки с крестиком ничего не нашла. Галочка стояла, сообщения не было. Галочка врала громче, чем молчание.
Починка: перестать хэшировать термометр вместе с температурой
Файл, который содержит собственную дату, — это слабая форма самоописания: он не может честно отчитаться о себе, потому что отчёт входит в предмет отчёта. Взвешивать чемодан вместе с весами можно, но узнать так вес чемодана нельзя.
Развожу на две величины:
- хэш существа — файл, в котором строка даты стёрта и заменена прочерком;
- дата — рядом, отдельным полем.
Вместе они по-прежнему покрывают файл целиком, зато теперь различают, что именно поехало. Второй случай стал достижим, и самопроверка на нём наконец покраснела своим сообщением, а не соседским. Заодно добавился четвёртый — про переводы, — и вот тут началось смешное.
Двадцать пять документов, соврамши
Правила сообщества живут на семнадцати языках. Английский оригинал я поправил, дату поднял.
Двадцать пять переводов остались датированы тринадцатым августа. Правились они, разумеется, двадцать седьмого — вместе с оригиналом, тем же коммитом.
Причём сам оригинал врал две недели: текст поменяли двадцать седьмого, а шапка невозмутимо сообщала тринадцатое. Документ, который люди принимают галочкой, две недели ошибался в собственном возрасте. Я так делаю в анкетах, но от юридического текста ждёшь большей собранности.
Правило для переводов взял тупое и потому работающее: все языковые версии документа обязаны датировать себя днём оригинала. Месяцы не сверяю принципиально — семнадцать языков склоняют их семнадцатью способами, а таблица названий месяцев сгниёт быстрее, чем поймает хоть что-нибудь. День и год — цифры везде, включая два языка, которые пишут год первым. Цифры и сверяю. Двадцать пять дат подняты, дальше за этим следит машина.
Хороший конец
Когда всё уехало и выложилось, я не стал верить ни себе, ни зелёному CI. Скачал документы с живого адреса — те самые, которые сейчас получит человек, — стёр в них строку даты, посчитал хэш и сравнил с тем, что написано в манифесте.
Шесть из шести. Вот теперь можно говорить «работает».
Что унести к себе
Первое. Ассерт на код возврата — это ассерт на то, что что-то случилось. Проверяйте сообщение, а не факт расстройства. Иначе однажды один сломанный случай будет закрывать галочкой соседний, и вы узнаете об этом в тот день, когда соседний понадобится.
Второе. Если X лежит внутри hash(X + Y), вы никогда не отличите изменение X от изменения Y. Звучит очевидно, пока не окажется, что X — это версия, а Y — то, что версионируют.
Третье, самое дешёвое. Пустая строка там, где должно быть сообщение, — это баг. Не косметика вывода, а баг. Мне повезло, что я на неё посмотрел; половина смысла писать подробные сообщения об ошибках в том, чтобы заметить, когда их нет.