Пока проблем с производительностью не испытывали. Хотя и сотни тысяч объектов в кадр не помещали.
Ну, про сотни тысяч я говорил не в кадре, а в сцене — понятно, что после отсечения отрисуется гораздо меньше. Просто это отсечение невидимых объектов тоже имеет ненулевую стоимость CPU. При этом описанные коллбэки и визиторы — это отличные ООП паттерны, но в классическом виде они очень плохо дружат с локальностью кэша, и на большом количестве объектов могут занимать ну очень существенное время. Поэтому и заинтересовался.
Кстати число фактических команд на отрисовку легко может вырасти в разы при появлении теней, локальных источников света (если только у вас не deferred shading) и отражающих поверхностей, типа зеркал.
У меня тот же тип дальтонизма, что и у автора, и цвета светофора я различаю отлично. Именно по цветам, а не положению фонаря. А вот в сумраке под столом различить синий и зеленый штекеры клавиатуры и мыши (был когда-то стандарт PS/2) — прям вообще проблема. Да и с картошкой тоже некоторое время думал, что жена стебется, показывая две одинаковые картофелины, и говоря, что «они же абсолютно разного цвета».
Огромное спасибо за упоминание приложения. До сих пор помню, как жена была в шоке от того, как я набирал и желтую и зеленую картошку, а потом я долго думал, что она прикалывается, показывая на две одинаковые картофелины и утверждая, что «они же абсолютно разного цвета».
Вот только вроде при запуске kitchen test кухня поднимает и тушит виртуалки по очереди, в отличие от молекулы. Или есть какой то обходной путь помимо руками вызвать kitchen converge && kitchen verify && kitchen destroy?
Небольшой вопрос — а вы не сталкивались с необходмостью перезагружать виртуалки в ролях? Просто в этом случае test kitchen теряет соединение, и говорит что test failed, в отличие от молекулы, которые отрабатывает такие ситуации нормально. Возможно вы как-то смогли это обойти?
Странно, как раз молекулой интеграционное тестирование нескольких ролей очень даже неплохо получается, за счет возможности нормально поднять одновременно несколько разных виртуалок, в отличие от test kitchen, которая в автоматическом режиме поднимает виртуалки по одной. Или я чего-то не знаю?
Это уже не юнит тест, вы тестируете связку двух методов.
Я тестирую часть контракта, и у кода нет внешних зависимостей — чем не юнит тест? То, что это связка двух методов — так если например вы будете тестировать даже банальный сеттер, то для проверки вы будете вынуждены либо использовать геттер, что даст опять же связку двух методов, либо лезть до приватного поля, что фуфуфу, поскольку это детали реализации и может сломаться при рефакторинге. Имхо почти всегда в юнит тестах у вас будет связка минимум двух методов.
К тому же ваш подход даст зеленый свет на таком коде
Возможно мне следовало уточнить, что предложенный мной тест не является исчерпывающим. Разумеется, необходимо проверять и другие вещи — как минимум неравенство encrypted != decrypted, невозможность расшифровать "чужим" ключом, и т.п. И да, я полностью согласен, что должна быть возможность инжектить nonce, чтобы можно было написать тест-кейсы на конкретные примеры.
В assertDecrypt мы прячем реализацию decrypt, которую рассматриваем как эталонную: наш контракт таков, что зашифрованные данные будут расшифрованы данной реализацией.
А потом, чтобы протестировать decrypt вы будете писать аналогичный assertEncrypt, в который спрячете "эталонный" encrypt? Но по факту это же будет два одинаковых теста?
вместо валидации контракта, мы валидируем обратимость функций
А разве контракт не подразумевает, что функция шифрования должна быть обратимой? В предложенном мной тесте именно эта часть контракта и валидируется, и если я вас правильно понял — в вашем варианте проверяется ровно та же часть контракта.
Для этого существуют фикстуры. Готовим нужное состояние системы под каждое действие и проверяем, что оно работает так, как задумано.
Фикстуры — да, полезны когда какое-то конкретное состояние системы может использоваться в куче разных тестов. Но я говорил про другое — когда есть объект какого-то класса, у него например вызывается два раза метод a(), потом три раза метод b(), а потом снова a() и тут его раскорячивает. Как по мне — вполне нормальный кейс для отдельного изолированного теста, воспроизводящего конкретный баг (при том, что есть также отдельные тесты на более простые ситуации).
Иначе мы просто впустую тратим время, инициализируя всё необходимое для всей цепочки вызовов, чтобы нужные состояния создавались самой системой, а не были захардкожены.
Несколько раз перечитал эту фразу, и не понял. Можете пояснить (желательно на примере)?
Это тоже test smell, когда методы тестируются не изолированно друг от друга.
Да, отстутвие изоляции между тестами — это фуфуфу. Но я предлагал совсем не это.
Хороший модульный тест своим фейлом говорит, что «вот этот конкретный метод не работает так, как задуман»
Имхо — хороший модульный тест своим фейлом говорит, что "вот этот конкретный сценарий работает не так, как задумано". И иногда этот сценарий — действительно вызов какого-то одного мутирующего метода, а иногда и цепочка действий. Плюс вы же состояние объекта надеюсь проверяете через публичные геттеры, так что даже банальное
У вас функция шифрует данные (пусть алгоритмом AES).
assert decrypt(encrypt(data)) == data и проверяете на самых разных data
Ваш класс генерирует параметризируемый mesh 3D-объекта. Пусть это будет куб с закругленными краями.
1) что куб — вообще на куб похож.
Как вариант — реализовать distance_to_cube как функцию расстояния до поверхности обычного куба, и проверять эти расстояния для всех вершин закругленного куба с аналогичными параметрами. Чуть более общий вариант — реализовать distance_to_mesh, генерировать простой куб без скруглений, ну и дальше аналогичная проверка.
2) что все полигоны обращены к наблюдателю, как ожидается
Например взять центральную точку куба, и для каждого треугольника assert dot(((a+b+c)/3 - center), cross(b-a, c-a)) >= 0.
3) не перепутаны индексы полигонов или текстурных координат
Топологические проверки очень неплохо делать с помощью half-edge структуры данных. Как минимум — если у вас в принципе по набору треугольников получилось построить half-edge граф, то сетка является 2-manifold (а в большинсте подобных случаем именно такая проверка и требуется).
Ваша функция увеличивает среднюю громкость аудиопотока. Как написать тест на это?
assert stddev(output) > stddev(input), где stddev — функция рассчета среднеквадратичного отклонения.
Вы реализовали генератор случайных чисел. Как его протестировать?
Если не считать очевидных ассертов на вхождение вывода генератора в некий диапазон, если это применимо, то как минимум можно посчитать среднее и СКО, причем лучше делать это примерно так:
for _ in range(min_iters):
stats.add(gen_under_test())
for _ in range(max_iters - min_iters):
if abs(stats.mean - expected_mean) < tolerance:
return
stats.add(gen_under_test())
assert False, "after maximum iterations mean {} was still far to off from expected {}".format(stats.mean, expected_mean)
где stats — некий статистический аккумулятор. Смысл — с одной стороны как можно раньше закончить тест, с другой — если прям сильно неудачные значения из генератора выскакивают, то можно было подождать подольше. Хотя в целом имеет право на существование и более простой вариант assert abs(mean(gen_under_test() for _ in range(iter_count)) - expected_mean) < tolerance. Аналогично можно проверять гистограмму на соответствие функции плотности вероятности.
Функция получает на вход дату рождения человека и возвращает его возраст. Как написать тест на такую функцию?
Видимо функция каким-то образом получает еще и текущую дату? Если через параметр (или если допустимо вынести текущую дату в параметр) — то ответ наверное очевиден — набором разных примеров-соответствий дата рождения — текущая дата — возраст. Если через какой-то глобальный get_current_date, то мокировать (а если невозможно, то выделять интерфейс и все равно мокировать) get_current_date, и дальше опять наверное очевидно.
Есть некий класс reader с двумя функциями: readChar(), isAvailable()
Если длина буфера заранее известна, то первое, что приходит в голову:
for _ in range(buf_size):
assert isAvailable()
readChar()
assert not isAvailable()
Но в идеале конечно лучше несколько раздельных тестов.
Люто плюсую, правда есть несколько моментов, которые хотелось бы прокомментировать.
Мое мнение — прошу не ассоциировать его со мнением моей команды, — что тесты нам помогают.
Т.е. не все в команде согласны, что тесты это хорошо? )
long_name
multiple_asserts
Не совсем согласен. Иногда бывает так, что тестируется какой-то особо хитрый corner case, который возникает после какой-то определенной последовательности действий. Причем добавлять ассерты в промежуточные моменты также бывает полезным, чтобы точно знать, что все действительно идет так, как ожидается. Понятно, что если такая ситуация возникает, то это сигнал к тому, что тестируемый объект вероятно надо как-то распиливать, но далеко не всегда это возможно сделать быстро, особенно если это еще и legacy.
При этом ассерты могут быть довольно сложные.
Если тестовый фреймворк позволяет посмотреть какая именно часть ассерта завалилась — то это ок. Если нет — то имхо лучше раздельные ассерты ради читабельности как самих ассертов, так и вывода, когда они таки упадут.
random
Плюс много. Еще могу добавить к перечисленному списку источников рандома — передача get_current_time в тестируемую функцию, которая ожидает время.
Правда есть отдельный случай, когда рандом имхо оправдан — это property based testing, но там во всех нормальных фреймворках всегда есть возможность воспроизводимости, как правило через seed.
thread_sleep
О да. И еще всякие while current_time < last_time + timeout в разных вариациях. Причем часто интерфейс можно очень легко сделать так, чтобы таймстемпы можно было инжектить, но к сожалению все равно находятся товарищи, которые говорят "фуу, только ради инжекта таймстемпов менять интерфейс".
@VisibleForTesting
Ну или лазание к "приватным" полям, в зависимости от языка (в питоне например такое любят). И это плохо не только тем, что юзкейсы в проде и в тестах становятся разными, но еще и в случае рефакторинга потрохов с гораздо большей долей вероятности придется менять тесты, чем если бы они были написаны только с использованием публичного интерфейса, который обычно хотя бы стараются держать стабильным.
Поправьте текстуры, и хало наконец уйдет (в фотошопе инструмент кажется defringe назывался, но могу ошибаться).
Кстати, судя по окнам локомотива альфа-тест у вас включен для всех текстур с альфа-каналом, что зря. Лучше все-таки разделять кейсы:
1) трава, деревья, возможно какие-то заборы — альфа-тест, в идеале — без альфа бленда, но со включенными MSAA, alpha to coverage и z-write, и можно не сортировать (но выводить все равно лучше после полностью непрозрачных объектов по соображениям производительности z-буфера)
2) действительно полупрозрачные объекты типа окон — z-test и альфа-бленд включен (и используется как раз premultiplied вариант), альфа-тест и z-write выключен, сортировка от дальнего к ближнему, выводить после полностью непрозрачных объектов
1) в исходной текстуре в прозрачных местах случайно не белый был? по хорошему в таких штуках цвет заливки должен хотя бы примерно совпадать с цветом границ, как например тут (смотреть на левую текстуру): us.v-cdn.net/5021068/uploads/editor/b0/5mlwn87s58ip.jpg
2) перед тем, как загрузить текстуры в видеокарту вы точно сделали предумножение rgb канала на альфу? В итоге в прозрачных местах rgb того, что лежит в видеопамяти должно быть черным.
Другой вариант — не использовать alpha blending вообще, вместо этого включить MSAA, и для листвы использовать режим alpha to coverage, эффект достаточно хороший, особенно при MSAA 4x и выше, при этом геометрию не обязательно сортировать — традиционный z-buffer при этом отлично работает. Минусы — если будете использовать полноэкранные эффекты и особенно технику deferred shading — включенный MSAA даст очень много мороки. Подробнее тут: medium.com/@bgolus/anti-aliased-alpha-test-the-esoteric-alpha-to-coverage-8b177335ae4f
Выглядит и правда странно, но если это все-таки проблема в порядке отрисовки, то по любому там еще и z-write не выключен на полупрозрачной геометрии, что по мне немного странное решение. С другой стороны — границы там далеко не все «прямые», хотя возможно это из-за дополнительного альфа-теста.
Спасибо за поправку, на телефоне автоисправление иногда очень неожиданно и незаметно срабатывает.
Насчет того, что premultiplied alpha не отменяет порядка отрисовки полностью согласен, просто не имея перед глазами конкретного скриншота не совсем понятно какого рода там артефакт. А неудачная текстура без premultiplied очень даже может давать светлые ореолы на границах, даже при корректном порядке.
Ну, про сотни тысяч я говорил не в кадре, а в сцене — понятно, что после отсечения отрисуется гораздо меньше. Просто это отсечение невидимых объектов тоже имеет ненулевую стоимость CPU. При этом описанные коллбэки и визиторы — это отличные ООП паттерны, но в классическом виде они очень плохо дружат с локальностью кэша, и на большом количестве объектов могут занимать ну очень существенное время. Поэтому и заинтересовался.
Кстати число фактических команд на отрисовку легко может вырасти в разы при появлении теней, локальных источников света (если только у вас не deferred shading) и отражающих поверхностей, типа зеркал.
А что с производительностью у такого решения? Особенно когда в области видимости пара тысяч объектов, а в полной сцене — десятки или сотни тысяч?
Вот только вроде при запуске kitchen test кухня поднимает и тушит виртуалки по очереди, в отличие от молекулы. Или есть какой то обходной путь помимо руками вызвать kitchen converge && kitchen verify && kitchen destroy?
Очень даже справляется. Пример можно посмотреть например тут: https://github.com/ansible/molecule/tree/master/test/scenarios/driver/docker/molecule/multi-node Для AWS или вагранта — аналогичные тесты в соседних директориях
Я тестирую часть контракта, и у кода нет внешних зависимостей — чем не юнит тест? То, что это связка двух методов — так если например вы будете тестировать даже банальный сеттер, то для проверки вы будете вынуждены либо использовать геттер, что даст опять же связку двух методов, либо лезть до приватного поля, что фуфуфу, поскольку это детали реализации и может сломаться при рефакторинге. Имхо почти всегда в юнит тестах у вас будет связка минимум двух методов.
Возможно мне следовало уточнить, что предложенный мной тест не является исчерпывающим. Разумеется, необходимо проверять и другие вещи — как минимум неравенство encrypted != decrypted, невозможность расшифровать "чужим" ключом, и т.п. И да, я полностью согласен, что должна быть возможность инжектить nonce, чтобы можно было написать тест-кейсы на конкретные примеры.
А потом, чтобы протестировать decrypt вы будете писать аналогичный assertEncrypt, в который спрячете "эталонный" encrypt? Но по факту это же будет два одинаковых теста?
А разве контракт не подразумевает, что функция шифрования должна быть обратимой? В предложенном мной тесте именно эта часть контракта и валидируется, и если я вас правильно понял — в вашем варианте проверяется ровно та же часть контракта.
Фикстуры — да, полезны когда какое-то конкретное состояние системы может использоваться в куче разных тестов. Но я говорил про другое — когда есть объект какого-то класса, у него например вызывается два раза метод a(), потом три раза метод b(), а потом снова a() и тут его раскорячивает. Как по мне — вполне нормальный кейс для отдельного изолированного теста, воспроизводящего конкретный баг (при том, что есть также отдельные тесты на более простые ситуации).
Несколько раз перечитал эту фразу, и не понял. Можете пояснить (желательно на примере)?
Да, отстутвие изоляции между тестами — это фуфуфу. Но я предлагал совсем не это.
Имхо — хороший модульный тест своим фейлом говорит, что "вот этот конкретный сценарий работает не так, как задумано". И иногда этот сценарий — действительно вызов какого-то одного мутирующего метода, а иногда и цепочка действий. Плюс вы же состояние объекта надеюсь проверяете через публичные геттеры, так что даже банальное
это проверка сразу пары методов класса.
assert decrypt(encrypt(data)) == data
и проверяете на самых разных dataКак вариант — реализовать distance_to_cube как функцию расстояния до поверхности обычного куба, и проверять эти расстояния для всех вершин закругленного куба с аналогичными параметрами. Чуть более общий вариант — реализовать distance_to_mesh, генерировать простой куб без скруглений, ну и дальше аналогичная проверка.
Например взять центральную точку куба, и для каждого треугольника
assert dot(((a+b+c)/3 - center), cross(b-a, c-a)) >= 0
.Топологические проверки очень неплохо делать с помощью half-edge структуры данных. Как минимум — если у вас в принципе по набору треугольников получилось построить half-edge граф, то сетка является 2-manifold (а в большинсте подобных случаем именно такая проверка и требуется).
assert stddev(output) > stddev(input)
, где stddev — функция рассчета среднеквадратичного отклонения.Если не считать очевидных ассертов на вхождение вывода генератора в некий диапазон, если это применимо, то как минимум можно посчитать среднее и СКО, причем лучше делать это примерно так:
где stats — некий статистический аккумулятор. Смысл — с одной стороны как можно раньше закончить тест, с другой — если прям сильно неудачные значения из генератора выскакивают, то можно было подождать подольше. Хотя в целом имеет право на существование и более простой вариант
assert abs(mean(gen_under_test() for _ in range(iter_count)) - expected_mean) < tolerance
. Аналогично можно проверять гистограмму на соответствие функции плотности вероятности.Видимо функция каким-то образом получает еще и текущую дату? Если через параметр (или если допустимо вынести текущую дату в параметр) — то ответ наверное очевиден — набором разных примеров-соответствий дата рождения — текущая дата — возраст. Если через какой-то глобальный get_current_date, то мокировать (а если невозможно, то выделять интерфейс и все равно мокировать) get_current_date, и дальше опять наверное очевидно.
Если длина буфера заранее известна, то первое, что приходит в голову:
Но в идеале конечно лучше несколько раздельных тестов.
del, ошибся веткой
Люто плюсую, правда есть несколько моментов, которые хотелось бы прокомментировать.
Т.е. не все в команде согласны, что тесты это хорошо? )
Не совсем согласен. Иногда бывает так, что тестируется какой-то особо хитрый corner case, который возникает после какой-то определенной последовательности действий. Причем добавлять ассерты в промежуточные моменты также бывает полезным, чтобы точно знать, что все действительно идет так, как ожидается. Понятно, что если такая ситуация возникает, то это сигнал к тому, что тестируемый объект вероятно надо как-то распиливать, но далеко не всегда это возможно сделать быстро, особенно если это еще и legacy.
Если тестовый фреймворк позволяет посмотреть какая именно часть ассерта завалилась — то это ок. Если нет — то имхо лучше раздельные ассерты ради читабельности как самих ассертов, так и вывода, когда они таки упадут.
Плюс много. Еще могу добавить к перечисленному списку источников рандома — передача
get_current_time
в тестируемую функцию, которая ожидает время.Правда есть отдельный случай, когда рандом имхо оправдан — это property based testing, но там во всех нормальных фреймворках всегда есть возможность воспроизводимости, как правило через seed.
О да. И еще всякие
while current_time < last_time + timeout
в разных вариациях. Причем часто интерфейс можно очень легко сделать так, чтобы таймстемпы можно было инжектить, но к сожалению все равно находятся товарищи, которые говорят "фуу, только ради инжекта таймстемпов менять интерфейс".Ну или лазание к "приватным" полям, в зависимости от языка (в питоне например такое любят). И это плохо не только тем, что юзкейсы в проде и в тестах становятся разными, но еще и в случае рефакторинга потрохов с гораздо большей долей вероятности придется менять тесты, чем если бы они были написаны только с использованием публичного интерфейса, который обычно хотя бы стараются держать стабильным.
Поправьте текстуры, и хало наконец уйдет (в фотошопе инструмент кажется defringe назывался, но могу ошибаться).
Кстати, судя по окнам локомотива альфа-тест у вас включен для всех текстур с альфа-каналом, что зря. Лучше все-таки разделять кейсы:
1) трава, деревья, возможно какие-то заборы — альфа-тест, в идеале — без альфа бленда, но со включенными MSAA, alpha to coverage и z-write, и можно не сортировать (но выводить все равно лучше после полностью непрозрачных объектов по соображениям производительности z-буфера)
2) действительно полупрозрачные объекты типа окон — z-test и альфа-бленд включен (и используется как раз premultiplied вариант), альфа-тест и z-write выключен, сортировка от дальнего к ближнему, выводить после полностью непрозрачных объектов
us.v-cdn.net/5021068/uploads/editor/b0/5mlwn87s58ip.jpg
2) перед тем, как загрузить текстуры в видеокарту вы точно сделали предумножение rgb канала на альфу? В итоге в прозрачных местах rgb того, что лежит в видеопамяти должно быть черным.
1) исходные текстуры преобразовать как (rgb, a) -> (a*rgb, a)
2) использовать бленд-функцию src * ONE + dst * (1 — SRC_ALPHA)
Почему именно так — можно почитать тут:
www.realtimerendering.com/blog/gpus-prefer-premultiplication
blogs.msdn.microsoft.com/shawnhar/2009/11/02/texture-filtering-alpha-cutouts
blogs.msdn.microsoft.com/shawnhar/2009/11/06/premultiplied-alpha
Другой вариант — не использовать alpha blending вообще, вместо этого включить MSAA, и для листвы использовать режим alpha to coverage, эффект достаточно хороший, особенно при MSAA 4x и выше, при этом геометрию не обязательно сортировать — традиционный z-buffer при этом отлично работает. Минусы — если будете использовать полноэкранные эффекты и особенно технику deferred shading — включенный MSAA даст очень много мороки. Подробнее тут:
medium.com/@bgolus/anti-aliased-alpha-test-the-esoteric-alpha-to-coverage-8b177335ae4f
Насчет того, что premultiplied alpha не отменяет порядка отрисовки полностью согласен, просто не имея перед глазами конкретного скриншота не совсем понятно какого рода там артефакт. А неудачная текстура без premultiplied очень даже может давать светлые ореолы на границах, даже при корректном порядке.