E.T. as in SDET

Третьего дня занимался понятным нечастым кодом.
Занятие оказалось неожиданно показательным.
Хорошо показывает, чем SD отличается от SDET, и почему последних особенно мало.
Консилиум решил, что пример годен для наружного употребления.

Возникла следующая боевая задача – написать нехитрый аллокатор, но с парой специальных требований.
Чтобы для небольших аллокаций, чтобы жил в разделяемой между процессами арене фиксированного размера, чтобы пережил многопоточность (формально многопроцессность), итп.
Из-за спецтребований и встала, собственно.

Начальная версия зажила довольно быстро.
Даже корректно отработала пару-тройку простых тестов после внедрения в основную программу.
(Аллокатор не общий, только для неких специальных данных, которых немного.)

Однако сбой в этом аллокаторе может привести к отказу в обслуживании.
Несмертельному, тк. откажет только спец-фича со спец-данными, но крайне неприятному.

Поэтому решил протестировать код особо цинично.
Показателен оказался именно процесс тестирования.

Код-ревью проделали, разумеется; но баги ловить оно помогает слабо.
Это понятно, даже жалких 250 строчек чужого кода во всех мелких падучих деталях взором пронзишь не вдруг.

Начал с того, что протестировал тупо цикл с 1000 случайных аллокаций.
Сразу поправил пару некритичных багов системы опечатка, сработали ассерты.

Продолжил циклом в 20000 случайных Alloc() и Free() в случайном порядке.
На этом выловилась пара более серьезных ошибок.

Одна была в ДНК дизайне.
Конкретно, напрочь не был прописан случай, когда полностью освобождалась страничка памяти, не являющаяся первой в списке частично свободных. Cписок свободных был вообще односвязный. :-)
Разумеется, сработали ассерты.
Устыдился, переделал, заработало.

Другая была однострочная, системы “промазал в формуле”.
Неправильно заполнялись начальные битовые маски для свободных аллокаций на страничке.
Заметил случайно, отлаживая первую ошибку – маски в отладчике не совпадали с задумкой.
Обратно исправил.

После подобных открытий вероятность off-by-one исключить решительно невозможно.
Поэтому следующим этапом тестирования заполнил аллокации псевдослучайным мусором, сохраняя seed.
И после каждых 10 операций стал проверять, все ли хорошо, не испортился ли драгоценный мусор по сравнению с эталоном.
Этот этап почему-то отработал сразу без единого сбоя.
Даже при длинах аллокаций строго 2^N, приводящих к “плотному” заполнению памяти.
Слегка удивился, тк. ожидал минимум один off-by-one баг.

В итоге рандомизированный тест наконец стал проходится так, как ожидалось.
Но что это говорит пытливому уму?

Только то, что ошибок четное количество, и они взаимоуничтожаются!!!
Причем одна из ошибок может быть в самом тесте.

Через это финальный этап тестирования оказался такой.
Специально внес в аллокатор код, симулирующий случайную ошибку.
Чтобы 1 раз на 1000 аллокаций возвращался сбитый на 1 указатель.
А тест взял и… сработал.

Легкое удивление удалось ликвидировать за пару минут, сделав счетчик аллокаций и печать слова “bug!” при симуляции сбоя.
На ~10000 аллокаций (rand()%1000) сработал ровно 0 раз – матожидание при равномерном распределении равно 10, но rand() об этом не знает.

Заменил на одинокий выстрел в случайный момент времени номер 5763, тест с длинами аллокаций 2^N начал падать.
Как и положено по уставу.

Кстати, со случайными длинами, и через это неплотным заполнением памяти, даже под выстрелами тест падать не начал.
Как опять-таки положено по уставу. (С высоким шансом затирается slack, а не следующая аллокация.)

Настоящие монстры SDET, уверен, придумали бы что-нибудь еще.
Однако надо еще дописывать все остальное, так что ограничился этими 4 этапами.

Мораль в целом простая, фактически азбучная.

1. Написал код, напиши тест.
2. Крепко подумай, что проверяет твой тест. (См. pow2 vs. np2 длины аллокаций.)
3. Сложно сделать серьезный боевой тест, сделай рандомизированный.
4. Написал тест, проверь тест. (См. умшыленное внесение ошибок.)
5. Даже если код работает, пройди его разок в отладчике. (См. случайно пойманный баг с масками.)

Когда вы следовали всем пунктам в последний раз?

  • Balmer

    Следовал такому прошлой осенью, когда писал автомаппер. Правда там рандомный тест не просто сделать :( Проверял на куче примеров просто.

  • http://pigmeich.livejournal.com/ Pigmeich

    Если я правильно помню у rand() распределение лучше если делить (правда MAXINT дописывать придеться), а не остаток брать. Особенно на больших числах.

    А вообще, переписать рандомайзер из Кнута, 2 том и не париться.

    Кстати вопрос, а всё же TDD или тест опосля?

  • shodan

    > переписать рандомайзер из Кнута

    Mersenne Twister.

    > Кстати вопрос, а всё же TDD или тест опосля?

    Серебряной пули нету.