"Тяжёлые ассерты"
От: Khimik  
Дата: 06.05.19 18:47
Оценка:
В этой теме
Автор: 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, после срабатывания мой ассерт выдаёт сообщение, я могу перейти к коду и дальше продолжить выполнение программы, или шагать дебагером.
"Ты должен сделать добро из зла, потому что его больше не из чего сделать". АБ Стругацкие.
Re: "Тяжёлые ассерты"
От: scf  
Дата: 06.05.19 19:36
Оценка:
Здравствуйте, Khimik, Вы писали:

K>И у меня из этого возникла некая концепция “тяжёлых ассертов”, которую я хочу рассказать.


property based testing?
https://dev.to/jdsteinhauser/intro-to-property-based-testing-2cj8
Re[2]: "Тяжёлые ассерты"
От: Khimik  
Дата: 07.05.19 15:46
Оценка:
Здравствуйте, scf, Вы писали:

scf>Здравствуйте, Khimik, Вы писали:


K>>И у меня из этого возникла некая концепция “тяжёлых ассертов”, которую я хочу рассказать.


scf>property based testing?

scf>https://dev.to/jdsteinhauser/intro-to-property-based-testing-2cj8

Я пытаюсь понять этот текст, мне кажется тут запутанным языком говорят о каких-то простых вещах. Ну вот чем у авторов отличается, например, passive test от property-based test?
"Ты должен сделать добро из зла, потому что его больше не из чего сделать". АБ Стругацкие.
Re[2]: "Тяжёлые ассерты"
От: Ikemefula Беларусь http://blogs.rsdn.org/ikemefula
Дата: 13.05.19 10:08
Оценка:
Здравствуйте, 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, что бы те уточнили последовательность, чего большей частью ждать приходится слишком долго — поиск ведется фактически в слепую.

И, скажем, если на проекте, как это модно, только авто-тесты или, что еще хуже, только авто-тесы-писаные-девелоперами, целая куча багов ждет свою жертву.
Отредактировано 13.05.2019 10:16 Pauel . Предыдущая версия .
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.