Comments 62
Сколько не думал, так и не смог решить задачу - допустим у нас есть embedded linux. Под капотом есть arm и какая то обвязка. А как собственно оттестировать работу с этой обвязкой? Эмулировать i2c, spi, serial, а поверх прикрутить виртуальные устройства вместо реальных датчиков и устройств.
Примерно как mook server для http.
Все, что выше тестируется легко. А вот уровень железных интерфейсов - боль. Не поделитесь?
Так тестировать собственно на реальном железе. Тем более что это модульные тесты, не интеграционные.
Могу высказать гипотезу как тестировать инициализацию и работы периферии в микроконтроллерах ARM.
Как всё работает с реальным железом?
Для того чтобы настроить какой-либо периферийный модуль, нужно записать байты в определенные регистры, которые лежат в памяти по определенному адресу. Чтобы иметь доступ к ним, над ними созданы структуры (например CAN_TypeDef, список которых можно найти в заголовочном файле от вендора.
В таком файле есть такая запись как правило:
#define CAN1 ((CAN_TypeDef *) CAN1_BASE)
т.е. какой-то адрес в памяти приводится к указателю на структуру, а потом по этому указателю осуществляется доступ к регистрам периферийного модуля.
Собственно идея: в тесте создать свой собственный экземпляр структуры CAN_TypeDef. Имея некоторые эталонные значения регистров, их можно сравнивать с тем, что получилось при инициализации в нашей тестовой структуре. Сам такое ещё не пробовал, потому что такой подход подразумевает собой какие-то ненормальные трудозатраты.
Проблема в том, что мне надо гонять фреймворк, драйвера к устройствам по CI в облаке. Т.е там может быть даже не ARM. Надо виртуально создать те же SPI/Serial интерфейсы и виртуальные устройства к ним. И как то оттестировать все.
Так что регистры сразу мимо. Виртуализация шин и устройств на Линукс нужна.
Ну то есть код лежит а github и тестируется job-ами на gitthub, где не ARM не подключенных устройств нету.
потому что такой подход подразумевает собой какие-то ненормальные трудозатраты.При этом спасает практически только от опечаток.
Интерфейсы I2C и SPI тестировать очень легко и делать это можно так так:
1--В обработчике прерываний по окончании отправки увеличивать счетчик отправленных байт.
2--запустить отправку
3--подождать прерывания окончания отправки
4--Убедиться что счетчик отправленных байт увеличился на нужное значение. Если да то тест пройден. Если нет то не было прерывания. Значит интерфейс не работает.
UART и CAN вообще можно тотально тестировать в режиме loopback.
Потом. В каждом нормальном I2C/ SPI чипе должен быть константный регистр ChipID. Если он читается в нужное значение значит SPI/I2C работает.
Может я что-то не понимаю, но давайте это попробуем наложить на кейс:
Есть github и проект в нем. Который раз в день собирается где то в облаке гитхаба.
JOB-а качает ubuntu-latest на armv7 где у нас даже нет даже близко SPI/I2C/UART/etc
Нам надо поднять эти интерфейсы виртуально(либо запилить свой образ где они есть), чтобы наше приложение через read/write/ioctl могло с ним работать.
Привязать под эти виртуальные интерфесы какие то виртуальные девайсы, которые читают что им пишут и шлют ответ
Соответственно in/out должен соответствовать ожидаемому
Боюсь не прирываний, ни тем более доступа к CPU и регистрам у нас тут нет.
Сейчас же я вижу исключительно возможность тестировать на конкретном "железе" и никак иначе
У гитхаба есть веб-хуки. Например все pull requests в проект OP-TEE тестируются на реальном железе.
Они свое "реальное железо" в облако github завезли? Установили в стойки датацентров и тестируют? :)
https://github.com/OP-TEE/optee_os/blob/master/.github/workflows/ci.yml
builds:
name: make (multi-platform)
runs-on: ubuntu-latest
container: jforissier/optee_os_ci
Те же контейнеры или qemu
Как же хорошо кичится невежеством, правда? Я ничего не говорил про github workflow. Так что зря вы полезли смотреть именно его. Я говорил про веб-хуки. Если бы вы открыли любой пулл риквест там, то увидели бы проверку от IBART. Вот, например: https://optee.mooo.com:5000/logs/OP-TEE/optee_os/5653/1124289966/b594c241daea2a2592eadb17189591bad9fe8b9c
И таки да, это исполнялось на реальном железе. И нет, не в стойках датацентра гихтаба.
Вы правда не читали то на что это написали? Попробуйте ещё раз.
Но мысль понятна, если мы пишем фреймворк, для работы с железом, куда прикрутится сторонними разработчиками любое железо, то надо тестировать его на реальном железе(а нам как бы интерфейсы Линукс тестировать) и виртуализировать железо.
Как бы тут сказать. Все веб программирование научилось виртуализироваться, разработчики чипов - эмулирует работу на fpga. И только в эмбеддед - будь добр припаять провода. Только хардкор.
А давайте я вам еще усложню задачу... В этом фрэймворке есть эдак 600 интегрированных модулей которые тестируются параллельно.
Ну например интеграция с Oracle, DB2, MySql, Postgrees, Keycloak, AWS, K8S, Camel и еще очень длинный список.... Ну все вместе потащим куда то на железо? Только виртуализация, образы, контейнеры и где приходит embedded - наступает боль...
Многообещающий заголовок, а в результате много неплохих тезисов и, собственно, ни слова о модульном тестировании в Embedded.
Студенты пишут рефераты? ;)
Вся надежда на комментарии ;)
Тдд на си это для людей с обилием времени. Как вообще кто то себе это представляет? Проект может собираться десятки минут, это не жс файл исполнить отдельный. Писать на каждый файл таргет в смейке?
Разговор был про TDD, не просто тесты. Оно подразумевает частое и постоянное переключение между кодом и тестами, частый их запуск. ЕМНИП там по рекомендациям цикл между запусками должен быть в пару минут. Я пробовал это делать в жабсе, пробовал в шарпе. В JS это скорее даже плюс, с его дурнотой наличие тестов сразу\до даже ускоряет, в C# все еще полезно, но уже начинает задалбывать, т.к. требуется ждать сборку и на это начинает уходит 20-30% времени.
А вот в С, целый проект пересобирать каждые пару минут уже попросту не возможно. Не говоря уже про то, что сами тесты обычно пишутся на том же самом С и их написание не блещет удобством и скоростью.
Добавить сюда, что обычно ембеддед экосистема это либо вимо-консольки, либо тормознутые, тупые и неудобные эклипсо-сетебобы с UI курильщика.Вопрос про CI\CD я думаю отпадает, он не касается конкретно TDD.
"А вот в SQLite", вот давайте на чистоту, тут все любят объективность, науку и ее методы. В свете этого - делать что то, потому, что так слделал кто-то, это дурнота. Не говоря уже про то, что я не видал в статьях про ТДД какие либо ссылки на исследования, говорящие, что они поднимают надежность в среднем на столько-то, удешевляют разработку на столько-то и т.д. и т.п.
Это все - нам так кажется и это норм, до того момента, пока не начинается "Сделаем так же как у тех крутых ребят, просто потому-что".
У них могут быть свои причины - как например, что это ответственное приложение, на которое опираются миллионы пользователей, которое постоянно развивается.Против типичной железки, которая не будет получать обновлений ПО с огромной долей вероятности вообще.
Тдд на си это для людей с обилием времени
Да, поддержка тестов в актуальном состоянии требует времени.
Писать на каждый файл таргет в смейке?
Если есть cmake - есть и ctest. Обмазать googletest, например, и запускать там где и собирали.
Тут другое, код нужно сразу писать кроссплатформенно, и взаимодействие с железом разве что через заглушки.
рекомендую книгу "Test-Driven Development for Embedded C", автор James W. Grenning. ...
Там подробно и с кучей примеров ...
Для юнит тестирования там всего один пример!
Пример тестирования мигания 16-ю светодиодами.
Кто-нибудь в здравом уме согласиться писать столько кода, как в той книге, чтобы протестировать правильно ли моргают ваши светодиоды?
Допускаю что этим можно заниматься в молодые годы, когда времени не жалко.
Проблема таких книг в том, что их авторы никогда не анализируют практическую целесообразность.
А ещё в книге абсолютно проигнорированы средства аппаратной отладки и тестирования. Метод HIL в этом плане гораздо мощнее.
Просто я ожидал от книги, судя по её названию, реальных и практичных примеров, а не отладку вывода некоего метафорического хелловорда в виде светодиодов.
Т.е. польза юнит тестирования не продемонстрирована, но продемонстрирована ужасная процедурная канитель.
Тестирование не может быть больше по объёму чем целевой код.
А главное не охвачена в принципе сама специфика железа встраиваемых систем и идеология его тестирования принятая в современных чипа (да книга довольно давно написана, может издание у меня старое)
А я вам могу привести в пример мою файловую систему STfs, в которой всего один интеграционный тест.
И мою систему вы сразу можете применять на STM32, а SQLite если просто так сумеете портировать на STM32, то можете это записать как тринадцатый подвиг Геракла, несмотря на все их тестирование.
Ну т.е. ваш пример нерелевантный. Вернее нерелевантный в контексте малых встраиваемых систем, где разработчик один, у него ограничен ресурс времени и ни с кем он в своём коде не конкурирует.
Но нередко (я бы даже сказал, чаще) можно встретить ровно обратное - когда прошивка какой-то железки пишется целой командой
Вот тут и не согласен. Вернее нет интереса углубляться в вопросы управления коллективными разработками.
Если юнит-тестирование нужно для этого, то так и нужно прямо писать.
Моя мысль проста - для индивидуального разработчика концепция юнит-тестирования неэффективна. Есть более эффективные и гибкие подходы.
Граница между необходимостью интеграционных и юнит-тестов зависит от сложности программы.
Таким образом вы признали наличие границы. И она как раз чуть выше малых систем. Заметим что автор статьи как раз малыми системами и занят.
Поэтому если он работает в коллективе, то юнит-тестирование проистекает не из технической необходимости, а из организационной.
Гораздо больше возможностей даёт SIL тестирование. Оно более точное, потому что не отрывается от платформы и более гибкое, потому что SIL тестирование сопровождается автоматическим рефакторингом архитектуры, а не отдельных функций. Это вершина TDD. Сложность проистекает из-за плохой архитектуры. SIL тестирование позволяет увидеть кривизну архитектуры.
"Смотря под какую ОС. На STM32 под Linux - вообще изи."
Если под stm32 подразумевались несколько линеек микроконтроллеров как это подразумевал @Indemsysто:
Чего вы несёте? Какой Линукс?
1--Нужна память для хранения кода с тестами. Часто можно услышать высказывание: "Я не буду добавлять тесты в сборку так как у меня мало flash памяти в микроконтроллере". Разруливается эта ситуация очень просто. Если все тесты не помещаются в NorFlash память то делим общее количество тестов на несколько сборок.
Разруливается она еще проще - через NFS, если на девайсе есть Линукс и/или сетевая часть. В данном случае и тесты, и код, и логи могут лежать под гитом, даже в одном коммите (на предмет "какого вчера работало, а сегодня нет"). Средства диагностики в данном случае можно взять любые, пересобрав их под свою платформу, размер логов не ограничен и т.п. "git diff" в логах работает волшебно, когда поправил паяльником и посмотрел, что получилось, надо только регуляркой временные метки вырезать (или конфиги логгера править), чтобы diff не срабатывал на различие строк в части микросекунд.
Кому интересно про настоящее тестирование в embedded, ищите по "software in the loop", "hardware in the loop", "unit testing LD_preload". А статья - вода, автор не в теме.
В симуляторе, том же qemu?
Team Lead может написать тесты, а инженер-программист разработает программные компоненты для прохождения этих тестов.
Вот с этим проблема та же, что и с ЕГЭ - код пишется не для того, чтобы быть красивым, понятным и эффективным, а для того, чтобы проходить тесты.
вот я думал-думал, как мне начать разработку без железа, и предполагал, что придется сделать макет на есп32, общающийся по uart.
(программа на компьютере на python, в железке на С. тесты скорее всего на компьютере)
А теперь понял, что надо их по tcp/ip разделить, тем более, что сам только что это прорабатывал.. :-)
И тесты решил попробовать сделать. Полезная статья.
Для снятия ответственности с программиста. Программисты должны быть сами заинтересованы в том, чтобы писать код с тестами. В этом случае они всегда смогут сказать:
Смотрите. Код, который я написал, прошел тесты позавчера. Значит я не причем в том, что сегодня прототип загорелся перед инвесторами
Вообще нет связи. Успешное прохождение тестов не является доказательством корректности кода, причём на этом факте обычно заостряют внимание примерно на первых страницах посвящённой вопросу литературы.
Как вы поняли, что всё работало? Только на основании того, что прошли тесты?
Немного отвлекаясь, в моём опыте было такое, что в микроконтроллере вис его заводской BIOS, когда совпадало так, что ровно в полночь вызывалась функция для вывода символа в отладочной печати. И было, что перегретая при пайке схема начинала глючить, когда слишком много единичек собиралось в регистре и ток по этой причине возрастал. А надо сдавать заказчику.
Можно, конечно, как один мой знакомый эмбедщик-программист, говорить, что “программа – вещь стабильная, ищите в железе”, и Рафик невиноватый. Но это не очень конструктивный подход для общего дела.
Я спорю конкретно с утверждением:
Код, который я написал, прошел тесты позавчера. Значит я не причем в том, что сегодня прототип загорелся перед инвесторами
Я отрицаю причинно-следственную связь между первой и второй его частями.
А тесты не являются бронежилетом. В такой метафоре тесты – это как бы учебная тревога. Которая сама по себе непосредственно не влияет на вероятность умереть от пули. Особенно если мы тренировались в приёмах стрелкового боя и достигли совершенства в построении с автоматами, а прилетела крылатая ракета.
Какой-такой BIOS в микроконтроллере?
*8--Когда практикуется тестирование кода, то и код естественным образом получается структурируемый, модульный, простой, понятный и переносимый.
один из любимых побочных эффектов тестов, тесты убивают спагетти
А если спагетти вот прям необходимо?
Например когда надо вот ни жить ни быть, но получить ещё 5-10% больше скорости?
Чтобы код просто успевал, например между прерываниями таймера 30к-100кГц.
В итоге вручную разворачиваются циклы, подбираются перестановки строк независящего кода, порой по месту вписывается асм-блоки? Код изобилует тонким тюнингом опций компилятора чуть ли не на каждую строку и тд.
В итоге получаем прям воплощение антипаттерна "функцию-бога"
Вот как такое покрыть модульными тестами?
И в частности как покрыть юнит тестами код вылизанный по работе с конкретными таймингами?
(просто интересен реальный опыт других опытных людей для таких крайних и "вырожденных" случаев)
Возьмите проц помощнее. Ну или разделите спагетти на отдельные методы, с более-менее смысловым разграничением, которое можно покрыть тестами. А компилятор все равно все методы заинлайнит.
А может и не нужно такие методы покрывать тестами? Имхо, и с тестами нужно меру соблюдать.
Юнит-тесты в основном для логики, те функций возвращающих что-то. Для полноценного тестирования нужен хороший HAL, тогда можно и тайминги тестировать.
Просто автор статьи немного искажает смысл всего действа .
В концепции TDD (Test Driven Development) юнит тестирование применяется не для того чтобы протестировать написанный код, а чтобы написать этот код!
Т.е. совершенно иная логика применения метода.
Вы пишите какой-то скелет своей функции или целого фирмваре (в юнит тестировании нет ограничений на размер юнитов на самом деле) и сразу начинаете тестировать его в тестировочном фреймворке, там и время замеряется и все что нужно. Конечно результат поначалу будет отрицательный и тут вы как заведенный начинаете рефакторинг и бесконечный повтор этих самых юнит тестов пока итерации рефакторинга, наращивания функциональности и тестирования не приведут к рабочему коду.
Секрет технологии в правильном тестировочном фреймворке.
А юниты это не юниты вашего софта, а ваш софт как юнит в тестировочном фреймворке. Просто примеры в книгах упрощают до предела и так выглядит словно ваш софт должен быть из юнитов.
Это я пересказал основную идею книги "Test-Driven Development for Embedded C"
И мое мнение, что тестировочный фреймворк в большинстве случаев для малых систем оказывается слишком обременительным и неэффективным.
Откуда вы это поняли?
Тестирование уже написанного кода в TDD называется тестированием Legacy code, т.е. кода без тестов, а не unit тестированием. Это разные процессы.
По сути статья автора не про unit тестирование, а про некое доморощенное тестирование.
Давайте строже подходить к определениям , так же строго как вы подходите к чистоте чужого "дурнопахущего" кода.
Вы повторяете ровно то что я сказал. Да, именно так, разработка кода ведётся путём тестирования. Не имеет значение в течении дня все делается или с перерывами в несколько лет. Это все равно юнит тестирование.
Но если вы код написали, а потом к нему написали тесты, то это не юнит тестирование. Это может быть верификационное тестирование, как с упомянутым вами SQLite. Я не нашёл прямых указаний что SQLite разрабатывается методом TDD. Это значит их тесты - это не юнит тесты.
С другой стороны юнит тестирование может обойтись без специального тестирующего фреймворка. Достаточно просто придерживаться ручного итеративного тестирования при разработке. Например при разработке считывателя 1-Wire написать модуль сбора статистики по считываниям (длительность, скорость реакции, количество попыток, количество ошибок, среднее, максимум , минимум и т.д.). И это будет юнит тестированием. Вот такое я внедряю прямо в релизное фирмваре. И это считаю удобнее чем отдельный тестировочный фреймворк.
Потому что иные тесты называются тестами Legacy code.
Будь они хоть юнит, хоть микро, хоть нано или пико тестами.
Ну так постулирует книга, на которую вы сами дали ссылки и по которой предложили учиться.
Спасибо кстати за ссылку.
Просто иногда надо прочитать целую книгу чтобы разобраться в западной терминологии. Тут надо учесть, что по интернетам гуляет большое количество интерпретаций авторов не понявших смысл TDD или не принявших TDD и растащивших его на фрагменты.
Собственно, в индустрии разработки ПО под легаси-кодом обычно и понимается не любой код без тестов, а именно что-то существующее в проекте с древних времён
Мы и есть индустрия. Не так ли? Я разрабатываю уже 35 лет, вы 15 . Как договоримся так и будет.
Моё мнение такое: Legacy code - это код написанный дедовским способом, он может быть написан минуту назад, а уже быть архаичным. TDD - современный способ. Это способ разработки на основе требований и юнит тестов .
Просто когда попытаетесь дать определение древности, то наткётесь на относительность этого определения. Какой-нибудь юнец скажет про код написанный до пандемии что он древний, а вы, скажем, назовёте древним код написанный до кризиса восьмого года. И будет непродуктивный спор о древности.
Кардинальное изменение в подходе к кодированию - это именно метод TDD, а не некое покрытие тестами, которого якобы раньше не было.
Покрытие тестами всегда было. Но TDD обязывает начинать с формулирования требований. И вот тут у большинства затык и начинается. Требования сформулировать трудно, управлять ими хлопотно, поэтому этот пункт как бы не замечают. Но с удовольствием отмечают что тестируемые функции маленькие юниты, и тестировать соответственно надо мало чего.
Вот пример моего юнит-тестирования:
![](https://habrastorage.org/getpro/habr/upload_files/5d8/a29/dc9/5d8a29dc998a1756116cc669ad06189c.png)
Жёлтый блок - это собственно разрабатываемая система управления лифтом. А вокруг неё тестирующая обвязка.
Проектирование начинается с создание тестирующей обвязки, и потом я делаю все чтобы она показала правильность реакции системы управления. Но после кодогенерации получатся исходники совершенно нечитабельные, их бы назвали очень дурно пахнущими, но за ними будущее и это воплощение TDD и юнит-тестирования.
И в частности как покрыть юнит тестами код вылизанный по работе с конкретными таймингами?
ifndef TESTING , например. То, что невозможно протестировать - исключить из тестов.
Модульное Тестирование в Embedded (или код без модульных тестов — Филькина грамота)