Как стать автором
Обновить

Комментарии 40

Фаза № 2. Научиться стабильно воспроизводить баг *

А куда ведет звездочка? У меня - никуда :-(

Конечно, лучше быть богатым и здоровым. Только вот это не всегда выполнимо :-(

В качестве примера могу рассказать свою историю "со звездочкой", когда я больше года не мог добиться воспроизводимости одного злобного бага в

простейшем локально работающем коде

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

Думаю, аналогичные истории найдутся почти у любого программиста со стажем. Так что предложенный алгоритм не плох, но иногда может натолкнуться на "прозу жизни". Возможно, под Вашей "звездочкой" как раз и планировался подобный намек? Если да, то прошу считать мой коммент не только поклевкой на заброшенную в статье наживку ("Какой самый сложный программный баг Вам удавалось починить? В чем, собственно, было дело?"), но и экспонатом в кунсткамеру "историй со звездочкой", которые не укладываются в описанную стройную схему...

Фаза № 2. Научиться стабильно воспроизводить баг *

А куда ведет звездочка? У меня - никуда :-(

Это – баг)

Это было захватывающе, спасибо за пример.

Был один баг у большого и очень важного клиента, который никак не воспроизводился у нас локально. После нескольких недель мучений и наконец-то организованной в выходные видеосессии с клиентом, кучей трассировочных логов и прочего удалось выяснить, что внутрифирменный вебсайт у клиента строился каким-то их визуальным конструктором сайтов из кирпичиков, и в результате главная страница состояла из более чем сотни фреймов, каждый из которых это довольно большое дерево объектов. А наша прога прерывала загрузку сайта, лазила по этим фреймам и деревьям, и создавала у себя аналогичные деревья через RPC и serialisation/deserialisation. И наш код сериализации не учитывал, что в дереве есть много ссылок на один объект (а внутри объекта еще много и т.д.) и получался офигительный оверхед по памяти и времени. В итоге их сайт грузился полторы-две минуты вместо секунд 7, если без нашей проги. Контракт был на волоске, но в последние выходные мы все же успели (у клиента было землетрясение и они не смогли вовремя расторгнуть контракт). А контракт был на несколько млн баксов.

Был один баг у большого и очень важного клиента, который никак не воспроизводился у нас локально


Гейзенбаг (плавающая ошибка) - программная ошибка, которая исчезает или меняет свои свойства при попытке её обнаружения.

Не тот случай. У нас не было доступа к их вебсайту - гребаная секретность.

Самый сложный баг - тот, который не удалось отловить)

Как неуловимого Джо)

А что вы собираетесь делать с багами, которые не воспроизводятся? Спихивать на QA/Support/Product/Marketing/HR/Board фразой "приходите, когда сможете воспроизвести"? У меня в этом году была пара очень неприятных багов связаных с параллелизмом, которые удалось пофиксить только медитациями на код и попытками понять, что там может пойти не так. Да после того, как я понял, в чем была проблема мне удалось придумать алгоритм воспроизведения, но до этого оно падало только при совпадении определенных условий, которые случались только в продакшене.

Да, многопоточность она такая. Но там даже если не падает, не факт что проблем нет на самом деле

У разработчиков встраиваемых систем программные ошибки часто переплетаются с аппаратными проблемами. И тут все становиться сложнее.
Вот например мой недавний кейс с мертвым временем. Решение остается неоднозначным таже когда все вроде бы ясно.
Будет ли ошибкой если у вас в сиcтеме с жестким реальным временем проскакивают микросекундные незапланированные задержки в ШИМ-е? Как бы и ничего страшного не происходит, все остается рабочим, но возникают дополнительные стрессы на силовые элементы, их ресурс незаметно глазу сокращается. В целом увеличивается количество ремонтов и издержки, прибыльность падает. И это всего лишь от неуловимых, спорадических практически необнаруживаемых ошибок в планировщике и таймингах прерываний. Там завязаны и промахи кэша, и выравнивание по границе памяти, и нагрузка по DMA и т.д. и т.п.
Вот как с таким бороться?

Вот как с таким бороться?

надо диаграммы временные состовлять, как вот здесь, например: Вторая проблема наложения операций во времени

и по этим диаграммам работать.

Если вы в коде не сможете гарантированно разнести события во времени и/или правильно обработать их наложение, то все пропало.

Вы смогли так просто проанализировать и построить диаграммы поскольку отказались от множества дополнительных сервисов и отладочных протоколов.
С другой стороны реализуй вы SIL, быстрый канал связи по USB или Wi-Fi, много логических отладочных каналов и протоколов, кто знает, может вы свой алгоритм определения давления многократно бы улучшили.

 может вы свой алгоритм определения давления многократно бы улучшили.

дело в том что я запускал ЧУЖОЙ алгоритм определения давления, как бы библиотечные функции (хоть и с исходным кодом) которые я должен был вызывать и кормить данными и принимать сигналы управления от них, я не мог его не улучшить, не ухудшить, но должен был обеспечить абсолютную диагностику на заданном железе.

У меня не Wi-Fi у меня Ethernet есть в другой железке в SPI через DMA, собираюсь, кстати написать про высокоскоростной SPI через DMA скоро.

После того как стали использовать Haskell, забыли про отладку, как страшный сон! Уже больше года не касался дебагера!

А код для МК можно покрыть модульными тестами? Там же куча всего на регистры завязано. Если только подменять значения, но в аппаратной реализации могут быть нюансы. Я вот помню ловил проблему зависания если не проинициализировать целиком таблицу прерываний (т.е. инициализировать только те что используются, остальные не трогать вообще), как такое тестировать? Код то верный.

Можно покрыть основную логику, когда в место железа подсовываешь Mock-объект

Модульные тесты - это если программированием занимается команда. Но если дивайс программируешь в одиночестве, то модульные тесты это как разговор сам с собой.

В embedded есть устоявшийся путь к упрощению отладки - аппаратный отладочный движок внедрённый в архитектуру процессора. Для программистов под PC такая штука недоступна, а в микроконтроллерах это прогрессирующая фича.
Современные трассировщики типа J-Trace позволяют быстро ловить плохо воспроизводимые баги.
Например был у меня вот такой случай. Сброс вочдога:

/*------------------------------------------------------------------------------
  Сброс таймера WDT
 ------------------------------------------------------------------------------*/
void WatchDog_refresh(void)
{
  WDOG_MemMapPtr WDOG = WDOG_BASE_PTR;
  __disable_interrupt();
  WDOG->REFRESH = 0xA602;
  WDOG->REFRESH = 0xB480;
  __enable_interrupt();
}

Поначалу в этой функции не было запрета прерываний и все было хорошо. Много лет было хорошо.
Но после некоторых изменений софта дивайсы стали сбрасываться в непредсказуемые моменты времени как то подозрительно часто. Какие только корреляции мы не обнаруживали для объяснения этого поведения. Вплоть до фаз луны и космического ионизирующего излучения.

Но системный подход с использованием трассировки позволили за пару суток зафиксировать баг, тормознуть процессор, по шагам пройти назад и увидеть что проблема была в прерываниях возникавших между двумя обращениями к регистру WDOG->REFRESH.
Вклинивание прерываний между этими обращениями вызывало холодный сброс по причине аппаратного сбоя.

Модульные тесты - это если программированием занимается команда. Но если дивайс программируешь в одиночестве, то модульные тесты это как разговор сам с собой.

Модульные тесты для меня, это в первую очередь метод защиты от регрессионных ошибок при изменении кода, который был написан много лет назад.

Работа железа, это конечно очень важно, но мне очень не нравится искать проблемы в не работающем оборудовании, а потом их находить в плохо отлаженном коде основной логики приложения.

Скорее всего разговор упрётся в понимание терминов, но я все же предположу что модульные тесты - это тесты отдельных простых функций. Типа функции парсинга строки и разбиения на токены или функции компрессии блока, или функции шифрации блока или функции перестановки элементов в списках и т.п.
Если все эти функции уже были протестированы, кем-то проверены и давно работают, то зачем для них держать модульные тесты и повторять их? Как эти функции могут измениться если их никто не трогал?
С другой стороны если вы переходите на другой чип и там используете новую библиотеку с плавающей точкой и там новый сопроцессор плавающей точки, то очевидно все расчётные функции начнут давать другой результат и придётся не модульные тесты проверять, а скурпулёзно переписывать или корректировать методику расчётов. Если фильтр Калмана работал хорошо на одной библиотеке с плавающей точкой, то чтобы перейти на другую (типа более быструю, но менее точную или на половинную точность) придётся поиграться с ключевыми коэффициентами.

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

Это к тому что в моей практике играют роль не модульные тесты, а интегрированные тесты. В каждом проекте есть множество встроенных тестов запускаемых по месту и определяющие общую работоспособность протоколов и алгоритмов. И модульные тесты никогда не писал и не даже не видел необходимости в них.

Скорее всего разговор упрётся в понимание терминов

Скорее всего. Наверно подобные тесты можно назвать и интреграционными. Хотя как по мне, модуль может содержать в себе другие модули, а так же логику их взаимодействия. Тогда как моя основная цель тестирования - разделить проверку работы бизнес логики от железа, а название, это дело десятое.

То что называется "бизнес логика" я окончательно перешёл делать в Matlab в графическую нотацию диаграмм состояний и потоков. Именно потому что некоторых ошибок логики взаимодействия требующих сложных тестов там сделать просто невозможно, как то тупиковых состояний, гонок состояний и проч.

Наверно у нас опять будет конфликт формулировок.

Для меня бизнес-логика в контексте разработки программно-аппаратных комплексов, это те функции, которые определяются изначальными требованиями к разрабатываемому устройству. Их конечно можно моделировать и в Matlab`е, но как потом из этих диаграмм получить реальный код на С++?

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

Я вижу софт для встраиваемых систем как многоуровневую систему.
Непосредственно с аппаратной частью работает BSP, через BSP с периферией работает middleware и RTOS. И уже всё это пользует прикладной софт. И каждый из этих уровней отлаживается по своему.

BSP отладить нормально можно только через SWD/JTAG/Tracing

RTOS отлаживается через плагины к IDE и встроенные софтовые отладочные средства. Middleware обычно стороннее и иногда имеет даже свои модульные тесты, например файловые системы.

А вот прикладной уровень уже через логи, симуляцию, и натурное тестирование поскольку дело имеем с внешним неподконтрольным миром.
Matlab генерит очень надёжные исходники на C из своих диаграмм. Об этом у меня написано несколько статей.

В Matlab Stateflow исключительно удобная визуальная симуляция. Симуляция проводится в тестовом окружении. Тестовое окружение - это те же диаграммы, но имитирующие некоторые аспекты внешнего мира. При этом не надо думать о качественном написании интерфейсов функций для облегчения тестирования и прочих атрибутах исходников. Вообще исходники смотреть не надо. Тестовое окружение отключается и диаграммы без изменений могут работать в режиме Software-in-the-Loop (SIL) . SIL в среде Matlab- наиболее мощный метод отладки для прикладного уровня из виденных мной в реальной жизни.

Вы в самом первом комментарии в этой ветке сказали, что "Модульные тесты - это если программированием занимается команда. Но если девайс программируешь в одиночестве, то модульные тесты это как разговор сам с собой. "

То, что вы описали разработку как многоуровневую системы, действительно будет классно работать. Вот только в одиночестве, такой объем работ не провернуть. Тогда как модульные тесты пишутся легко в одиночку и решают большинство возникающих проблем (по правилу Парето)

А оставшееся меньшинство проблем занимают большинство времени  (по правилу Парето). Шучу.

А оставшееся меньшинство проблем занимают большинство времени (по правилу Парето). Шучу.

Вы не поверите!

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

Я написал несколько модульных тестов для выявления данного дефекта, включая эмулятор DMA (Mock объект для тестирования). Но к сожалению это не принесло результатов. И хотя некоторые ошибки действительно были найдены и исправлены, но щелчки все равно оставались.

И когда в последнее время щелчки стали реально очень громкими, то проблема была эскалирована для срочного исправления. В результате этот долгоиграющий баг был устранен, хотя проблема оказалась вовсе не в ПО, а железе (сложная зависимость работы УНЧ при пониженном напряжении питания + ошибка в монтаже в последних партиях оборудования).

Поэтому, да, Подписываюсь под словами, что оставшееся меньшинство проблем занимают большинство времени (по правилу Парето). Не шучу. :-)

BSP отладить нормально можно только через SWD/JTAG/Tracing

RTOS отлаживается через плагины к IDE и встроенные софтовые отладочные средства. Middleware обычно стороннее и иногда имеет даже свои модульные тесты, например файловые системы.

А вот прикладной уровень уже через логи, симуляцию, и натурное тестирование поскольку дело имеем с внешним неподконтрольным миром.

У меня в прошивке есть UART-CLI и я всё (MCAL,RTOS,App) полноценно могу отладить одной только UART-CLI.
https://habr.com/ru/articles/694408/

Речь не о принципиальной возможности что-то отладить, а о времени требуемом на отладку. Т.е. эффективности работы.
Как один из минусов отладочного UART-а и протоколов поверх него есть то, что называется "местный эффект", т.е. изменение поведения софта вследствие своего присутствия.
Я для старта от голого железа UART-ы давно не применяю, только RTT. RTT тоже что-то требует, но его можно оставить и заморозить местный эффект если он появился.
UART сильно тормозит отлаживаемые процессы, а если делать его асинхронным, то будет вмешиваться в тайминги прерываний. Интерфейс RTT прерываний не требует.
А отладку по SIL выполняю через толстые каналы типа USB или Wi-Fi.

Модульные тесты - это если программированием занимается команда. Но если дивайс программируешь в одиночестве, то модульные тесты это как разговор сам с собой.

Модульные тесты необходимы когда код мигрирует на другое окружение: новое железо или другой компилятор.
https://www.youtube.com/watch?v=-hM38-JDt8c&t=1653s

А код для МК можно покрыть модульными тестами? 

Легко! Вот модульный тест на то, что SPI в самом деле отправляет данные.

static bool test_spi_write_num(uint8_t num) {
    LOG_INFO(TEST, "%s(): SPI %u", __FUNCTION__,num);
    uint8_t array[2];
    bool res = true;
    SpiHandle_t* Node = SpiGetNode(num);
    EXPECT_NE(NULL, Node);
    uint32_t init_int_cnt = Node->it_cnt;
    memset(array,0xFF,sizeof(array));
    ASSERT_TRUE(spi_api_write(num, array, sizeof(array)));
    EXPECT_GR(init_int_cnt, Node->it_cnt);
    return res;
}

Интересный пример сразу раскрывающий сложность.

А сложность в том что такие тесты затрудняют рефакторинг. Рефакторинг API доступа к периферии. Одно из замечательных сопутствующих действий во время отладки - рефакторинг. Рефакторинг - это редактирование имён, изменение структуры аргументов, изменение состава функций.

И вот наличие таких избыточных тестов приводят к увеличению объёма рефакторинга, когда его хочется сделать, т.е. к увеличению рутины. Нет ничего хуже рутины в программировании. Рутина, замыливание внимания - серьёзная причина ошибок.

Рефакторинг - это редактирование имён, изменение структуры аргументов, изменение состава функций.

Все понимают рефакторинг по-своему. Для меня рефакторинг - это упрощение проекта.
Когда код простой, то и ломаться не чему.

Но я так понял что у вас нет простых проектов, поскольку вы вынуждены тянуть с ними кучу модульных тестов и поддерживать их.

Рефакторинг API доступа к периферии.

API доступа к периферии (MCAL) должен быть унифицированным для всех микроконтроллеров. Тогда код MCAL окажется переносимым например с STM32 на ESP32 или Artery.
Те же наборы модульных тестов будут работать. Та же документация будет актуальна.

Интересно кто создает эту унификацию.
В тексте вашей функции унификации не видно.
Такое имя как spi_api_write прям просится под рефакторинг.

Такое имя как spi_api_write прям просится под рефакторинг.

Хорошо. Как Вы бы назвали такую функцию?

Сам я отрицаю идею унификации, но приходится работать с исходниками команд где унификация возведена в культ.

И вот у них API для SPI выглядит так.

Это динамическая структура, т.е. инстансу этой структуры вы можете дать какое угодно имя. Имена указателей на функции внутри структуры одинаковы для целого класса периферии (UART, I2C, SDIO, GPIO, ...)

Получаем свободу в выборе имени инстанса, и унификацию в именах членов инстанса. Ребята реально поработали над унификацией.

Гейзенбаг (плавающая ошибка) - программная ошибка, которая исчезает или меняет свои свойства при попытке её обнаружения.

в квантовой физике есть какой то термин который обозначает-выражает эфект изменение параметров системы при попытке наблюдения за ней, ни как не могу вспомнить это слово, может кто подскажет?

Такое красивое слово- эффектное есть, гораздо лучше чем про баг.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации