В
этой темеАвтор: Khimik
Дата: 03.01.19
я писал про прямые и обратные задачи в программировании. Например, работа нейросети – это решение прямой задачи, а её обучение – решение обратной. Решение обратной задачи всегда сложнее.
И у меня из этого возникла некая концепция “тяжёлых ассертов”, которую я хочу рассказать.
Как я понимаю, ассертами можно назвать решение прямой задачи, позволяющее на отдельных этапах контролировать правильность решения обратной. Так вот “тяжёлые” ассерты – это такие, которые проверяют всё целиком, но очень тормозят программу, поэтому для их запуска нужен отдельный режим “тяжёлой отладки”.
Мой пример. Есть двумерный динамический массив, непрямоугольный. В нём N элементов; в каждом i-м элементе (от 0 до N-1) есть два массива одинаковой длины (Mi) с интегерами. В этих двух массивах располагаются уникальные индексы: номера от 1 до Mi, без повторов, в разной последовательности. Во втором массиве – обратные индексы: т.е. если в первом, например,
1 3 5 2 4, то во втором
1 4 2 5 3.
В программе периодически меняются числа в одном из этих “спаренных” массивов, и нужно автоматом поменять второй, чтобы сохранилось соответствие. Так вот обычный ассерт – это проверять только i-ю пару, а тяжёлый ассерт – проверять весь массив целиком.
Обычный ассерт быстрее, но тяжёлый проще и универсальнее. Это пример слишком простой, но будет понятнее например так: если имеется не просто двумерный массив, а какой-то сложный иерархический массив, с разными типами данных в каждой ячейке, рекурсией и т.д.; тогда простой ассерт потребуется вставлять в отдельный участок, и вообще этих простых ассертов будет много, а тяжёлый ассерт также будет единственный и надёжный. Только он будет очень тормозить.
Отсюда примерно такой общий подход к оптимизации алгоритма: сначала пишете медленный, но простой и надёжный код, потом делаете его более сложным, и в разных местах вставляете тяжелые ассерты: они должны проверять, что глючный быстрый код работает точно так же, как надёжный медленный.
Вот пример такого кода в моей программе (извиняюсь что стиль сильно нестандартный):
function TLinearTrianlesPlaneUnpackedModel.CalculateQDerivByPointVal(pointx,pointy:integer):double;
var
oldpointval:double;
firstq,altq:double;
result1:double;
function DoCalcQ:double;
begin
result:=CalculateQPart(pointx,pointy);
end;
begin
firstq:=DoCalcQ;
oldpointval:=fpoints[pointy][pointx];
fpoints[pointy][pointx]:=fpoints[pointy][pointx]+valshift;
altq:=DoCalcQ;
result:=(altq-firstq)/valshift;
fpoints[pointy][pointx]:=oldpointval;
if poHeavyDebugMode then begin
result1:=CalculateQDerivByPointVal_simple(pointx,pointy);
if not equal(result,result1) then assert(false);
end;
end;
Это код для МНК-минимизации через градиентный спуск. Для каждого параметра считается градиент; это можно делать "брутфорсово" (поменяли параметр, посмотрели как изменился весь функционал) или более сложно. Код после if poHeavyDebugMode сравнивает результат по брутфорсовому методу и сложному.
Здесь есть момент – фича Delphi (я не знаю, как с этим в других языках). Насколько я знаю, если задать константу poHeavyDebugMode=false, то весь проверочный код станет невидимым для компилятора, т.е. не будет выполняться даже проверка if. По-моему, этой фичи не будет, если константа имеет конкретный тип.
Ещё я перезагрузил стандартную процедуру assert в Delphi, после срабатывания мой ассерт выдаёт сообщение, я могу перейти к коду и дальше продолжить выполнение программы, или шагать дебагером.
"Ты должен сделать добро из зла, потому что его больше не из чего сделать". АБ Стругацкие.
Здравствуйте, scf, Вы писали:
scf>Здравствуйте, Khimik, Вы писали:
K>>И у меня из этого возникла некая концепция “тяжёлых ассертов”, которую я хочу рассказать.
scf>property based testing?
scf>https://dev.to/jdsteinhauser/intro-to-property-based-testing-2cj8
Я пытаюсь понять этот текст, мне кажется тут запутанным языком говорят о каких-то простых вещах. Ну вот чем у авторов отличается, например, passive test от property-based test?
"Ты должен сделать добро из зла, потому что его больше не из чего сделать". АБ Стругацкие.
Здравствуйте, scf, Вы писали:
K>>И у меня из этого возникла некая концепция “тяжёлых ассертов”, которую я хочу рассказать.
scf>property based testing?
scf>https://dev.to/jdsteinhauser/intro-to-property-based-testing-2cj8
Ассерты это совсем не тесты. Ассерты это проверка предусловий, постусловий, инвариантов в самом коде. Если ассерты легкие, они работают всегда. Если тяжелые, только по требованию, например, на стейджинге.
Разница между ассертами и тестами в том, что тесты проверяют небольшое ограниченное количество условий, ассерты проверяют всё, что проходит через функцию.
Например, у тебя есть функция, которая работает с BigInt, принимает BigInt и возвращает некоторый результат, который должен обладать определнными свойствами. Сколько тестов ты можешь себе позволить ? Ну 10, ну 100, может даже 1000. То есть, тесты очень дискретны — на фоне всего диапазона BigInt это почти ничего.
Для ассерта нужна функция, которая проверит, скажем, инвариант цикла или пост-условие, и будет делать это всегда, для всего диапазона BigInt.
Соответсвенно такой подход позволяет обнаружить совсем другой класс ошибок, который автоматическими тестами никак не покрывается. В свою очередь ассерты так же не покрывают весь класс ошибок, который обнаруживается тестами.
Следовательно, по уму, должны быть и тесты, и ассерты, они дополняют друг друга.
Пример, допустим, у нас есть внятное логирование
"операция x в очереди"
"операция x стартовала"
"операция x перешла в состояние XYZ"
"assert: ожидаемое состояние операции ZYX"
"операция x завершилась"
Для документирования ошибок необходимо найти все девиации
1 операции которые завершились, но почему то не стартовали — потенциально, некто неизвестные шедулит операции кастомным кодом
2 операции, которые стартовали, но не завершились, — не ясно, то ли зависло, то ли есть кастомный код, который завершает, то ли операция обрывается и дохнет
..
n паттерны assert: — ищем нарушения контрактов и тд и тд
Раз в день проводим мониториг логов стейджинга, все девиации должны быть задокументированы в багтрекере. Желательно иметь внятное соглашение об именовании, что бы можно было пользоваться поиском дубликатов
Скажем, у нас на старом проекте QA научились смотреть в логи и делать первичный анализ, если обнаруживают девиацию. Смотрят трассу и говорят — логин пошел дважды, первые тридцать-сорок секунд юзер как бы залогинен дважды, после перезапуска приложения, восстанавливается состояние второй сессии.
Теперь девелопер уже изначально знает чего искать — повторый логин.
Далее, QA уже сознательно пытаются найти эти кейсы, которые дают двойной логин, и обнаруживается, что "изредка" на самом деле совсем не "изредка", а зависит окружения пользователя. У некоторых будет не только "изредка", но и "частенько", и даже "постоянно".
Если всего этого нет, то баг выглядит совершенно иначе: "при перезапуске изредка приложение теряет данные пользователя". Типичная реакция девелопера — потыкать там-сям, от пяти минут до часа и закрыть "не воспроизводится". В лучшем случае отдать QA, что бы те уточнили последовательность, чего большей частью ждать приходится слишком долго — поиск ведется фактически в слепую.
И, скажем, если на проекте, как это модно, только авто-тесты или, что еще хуже, только авто-тесы-писаные-девелоперами, целая куча багов ждет свою жертву.