Печатные публикации

#9: Применение реинжиниринга при проектировании встраиваемых систем. Часть 2

Анна Сергеева. Применение реинжиниринга при проектировании встраиваемых систем. Часть 2 // Компоненты и технологии. 2014. № 10. С. 125-128.


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


Опубликовано в разделе "Встраиваемые системы"


Окончание. Начало в № 9`2014

Эта же статья на сайте журнала



Использование систем перезаписи кода для тестового покрытия


Как упоминалось ранее, программные средства преобразования кода, такие как, например, DMS Software Reengineering Toolkit, решают широкий спектр задач, связанных с процессом проведения реинжиниринга программных систем. Среди них, помимо непосредственного перевода исходного кода программ с одних языков на другие, существует еще немалое число возможностей сбора и обработки разного рода аналитических данных.


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


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


Инструменты тестового покрытия также можно применять для локализации функциональных возможностей программных приложений. При таком варианте использования инструмент тестового покрытия находит и указывает ту часть программного кода, который исполняется для предоставления конкретной выбранной оператором функции приложения. Стоит отметить, что это достаточно эффективный способ выяснения конкретных функциональных возможностей в крупномасштабных системах с запутанной структурой.


Особенности тестового покрытия для промышленных систем


При проведении анализа качества разрабатываемого программного кода одной из важных характеристик полноты тестирования является показатель покрытия ветвей (альтернатив) кода исполненными тестами (так называемыми «тестовыми пробами»).


Для кода, созданного на наиболее распространенных языках (C, Java, Verilog) и исполняемого на наиболее распространенных платформах (Solaris, UNIX), для сбора подобной информации существует целый ряд специально предназначенных инструментальных средств.


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


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


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


Подходы к обеспечению тестового покрытия


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


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


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


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


Существует два принципиальных подхода к обеспечению тестового покрытия: модификация объектного кода и модификация исходного кода.


Модификация исходного кода


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


В листинге 7 приведена программа (классический пример вычисления чисел Фибоначчи) на языке C, в которую добавлены и выделены курсивом тестовые пробы.


Листинг 7. Модификация исходного кода

int fibcache[1000]; // изначально обнулен


int fib(int i) // быстрое вычисление чисел Фибоначчи

{ int t;

visited[1]=1;

switch (i)

{ case 0: visited[2]=1;

case 1: visited[3]=1;

return 1;

default:

visited[4]=1;

if (fibcache(i))

{ visited[5]=1;

return fibcache(i);}

else { visited[6]=1;

t=fib(i-1);

fibcache(i)=t+fib(i-2)

return fibcache(i);

};

};

visited[7]=1;

};


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


При инициализации программы все флаги сбрасываются, и далее в ходе исполнения программы — устанавливаются для пройденных блоков кода. Затем, с помощью дополнительных средств, выполняется сбор и обработка результатов.


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


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


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


Модификация объектного кода


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


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


Ограничения для нестандартных программных систем


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


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


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


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


Практика показывает, что более удобным средством внедрения тестовых проб в код является использование автоматизированных систем преобразования исходного кода (source-to-source transformation systems).


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


Решение задачи тестового покрытия средствами инструментария DMS


Рассмотрим, как задача тестового покрытия может быть решена средствами инструментария, предоставляемого системой преобразования исходного программного кода DMS Software Reengineering Toolkit, о котором уже шла речь в данной статье.


DMS работает по следующему алгоритму.



  • До запуска компиляции и исполнения, код исследуемых исходных файлов разбивается на базовые блоки. В каждый из них вставляются тестовые пробы, написанные на языке исходных файлов.

  • Во время исполнения программы происходит срабатывание тестовых проб в тех блоках кода, которые были исполнены. Таким образом, формируются так называемые данные тестового покрытия (coverage data).

  • После завершения исполнения программы, все данные тестового покрытия попадают в файл отчета о тестовом покрытии.

  • Также данные тестового покрытия отображаются пользователю на интерфейсе системы DMS (рис. 3). По этим данным инженер, выполняющий тестирование системы, может наглядно видеть, какой код был исполнен, а какой нет. Также ему предоставляется полная статистика по собранным данным о тестовом покрытии.


Рис.3. Просмотр отчета данных тестового покрытия в системе DMS


При этом, система DMS способна обеспечивать оценку тестового покрытия для программ, написанных на любых процедурных языках кодирования.


Пример преобразования кода для оценки тестового покрытия


Далее приводятся примеры преобразования кода на языке C для обеспечения тестового покрытия средствами системы DMS.


На первом этапе, составляется перечень правил, используемых для перезаписи кода (листинг 8).


Листинг 8. Правила для преобразования кода программ на языке C

external pattern new_place

(x:statement_sequence).


rule mark_function_entry

(result:type,

name:identifier,

decls:declaration_list,

stmts:statement_sequence) =

\result \name { \decls \stmts };”

rewrites to

\result \name

{ \decls

{ visited[\new_place\(\stmts\)]=1;

\stmts }};”.


rule mark_if_then_else

(condition:expression;

tstmt:statement;estmt:statement) =

if (\condition)\tstmt else \estmt;”

rewrites to

if (\condition)

{ visited[\new_place\(\tstmt\)]=1;

\tstmt}

else {visited[\new_place\(\estmt\)]=1;

\estmt};”.


rule mark_switch_case

(condition:expression,

stmts:statement_sequence) =

case \e: \stmts”

rewrites to

case \e:

{ visited[\new_place\(\stmts\)]=1;

\stmts }”.


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


Также составляется спецификация вычислителя атрибутов для определения используемых констант (листинг 9).


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

ATTRIBUTES { SetOfReal Constants; }

Statement = ‘return’ Expression ;

<<UseConstants>> { Statement.Constants = Expression.Constants; }

Expression = Sum ;

<<UseConstants>> { Expression.Constants = Sum.Constants; }

Sum = Sum ‘-’ Term ;

<<UseConstants>> { Sum[0].Constants = UnionReals(Sum[1].Constants,Term.Constants); }

Sum = Sum ‘+’ Term ;

<<UseConstants>> { Sum[0].Constants = UnionReals(Sum[1].Constants,Term.Constants); }

Term = ‘(‘ Expression ‘)’ ;

<<UseConstants>> { Term.Constants = Expression.Constants };

Term = IDENTIFIER ;

<<UseConstants>> { Term.Constants = EmptySetOfReals(); }

Term = NATURAL ;

<<UseConstants>> { Term.Constants = SingletonRealSetFromNatural(NATURAL.); }

Term = DOUBLE ;

<<UseConstants>> { Term.Constants = SingletonRealSetFromDouble(DOUBLE.); }

Term = IDENTIFER ‘(‘ ArgumentList ‘)’ ;

<<UseConstants>> { Term.Constants = ArgumentList.Constants; }

ArgumentList = Expression ;

<<UseConstants>> { ArgumentList.Constants = Expression.Constants; }

ArgumentList = ArgumentList ‘,’ Expression ;

<<UseConstants>> { ArgumentList[0].Constants = UnionReals(ArgumentList[1].Constants,

Expression.Constants); }


По сути, для каждого выделенного в коде синтаксического фрагмента передачи управления нужно выполнить всего одно преобразование. И каждое такое преобразование состоит из двух этапов.


Сначала для каждого выполняемого преобразования выделяется новая нумерованная область рабочего пространства. Функция построения абстрактных деревьев new_place генерирует новый идентификатор области при каждом вызове. Присваивает этот идентификатор обрабатываемой ветке конкретного дерева и связывает его с файлом исходного кода и номером строки в этом файле.


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


На втором этапе, в код внедряются тестовые пробы. И здесь принципиально важно соблюдение такого принципа: если в обрабатываемом участке кода встречается условный блок и он содержит инструкцию передачи управления (листинг 10), то тестовая проба должна включаться в код обязательно до любого оператора, следующего за условным блоком (листинг 11).


Листинг 10. Передача управления из условного блока кода

if (<условие>)

{ x=y;

return;

}

<дальнейший код>


Листинг 11. Внедрение тестовой пробы после условного блока кода

if (<условие>)

{ x=y;

return;

}

visited[<указатель_местонахождения_пробы>]=true;

<дальнейший код>


Это ограничение касается незначительного числа правил и необходимо для учета всех случаев передачи управления, включая случаи передачи управления из условных блоков. Для краткости, подробности этих преобразований в данной статье не рассматриваются. Применение таких трансформаций к исходному программному коду дает результат, представленный ранее в листинге 7 (в котором был представлен пример вычисления чисел Фибоначчи). Аналогичным образом, достаточно легко можно придумать и другие интересные типы проб. Например, при обработке преобразований можно выполнять инкремент счетчика visited и таким способом использовать пробы не для тестового покрытия, а для профилирования.


Дополнительные замечания по инфраструктуре


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


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


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


  • Однострочное объявление массива visited. Инструмент преобразования кода может сообщить о наибольшем идентификаторе new place number, исходя их размера массива после внедрения тестовых проб.

  • Функция инициализации, которая сбрасывает массив visited при запуске тестирования исследуемой системы.

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


Во-вторых, система хранения результатов должна накапливать информацию о результатах нескольких проведенных тестах.


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


Для профилирования же значение массива visited нужно складывать поэлементно и добавлять к суммарному результату.


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


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


Можно использовать функциональные возможности системы DMS, обеспечивающие наглядное отображение результатов. Можно представить файлы в удобочитаемом виде в формате HTML, а также выполнить многоцветную подсветку участков кода по принципу пройдено/не пройдено в ходе тестирования.


Принимая во внимание квалификацию специалистов группы тестирования, реализация и внедрении всей этой дополнительной инфраструктуры представляется достаточно несложной. На рисунке 3 представлена реализация такого инструмента отображения результатов тестового покрытия на языке Java. Здесь можно просмотреть результаты покрытия по любому выбранному файлу, увидеть строки кода, в которые были вставлены тестовые пробы, а также оценить статус покрытия для конкретных тестовых проб.


Дополнительно, данный инструмент отображения включает следующую полезную возможность: калькулятор двоичных битовых векторов для объединения векторов по OR, AND и AND NOT. Это помогает инженеру, проводящему тестирование, легко определять, какие наборы тестов перекрывают друг друга, и какие тесты являются дубликатами других тестов в рамках одного или нескольких наборов тестов.


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


Практические результаты тестового покрытия от DMS


Описанная технология реализована разработчиками DMS в инструментальных средствах преобразования исходного программного кода и позволяет решать задачи тестового покрытия кода, написанного на следующих языках:


  • ANSI 89 C. Для обработки директив препроцессора используется специальный внешний интерфейс. Для других диалектов языка, таких как GNU C, Microsoft Visual C++ и ANSI 99 C, подразумевается ряд простых расширений. Поддержка языка C++ требует большего числа правил, но базовые принципы те же.

  • ANSI COBOL 85.

  • Java 1.1 и Java 1.3.

  • PARLANSE (внутренний язык реализации системы DMS).


Поддержка каждого из языков требует использования отдельного набора преобразований для внедрения тестовых проб, характерных для конкретного языка. Однако, все они используют один и тот же инструмент отображения результатов проведенного тестового покрытия (он описан в предыдущем разделе, и его интерфейс показан на рис. 3). С его помощью были успешно проведены практические испытания работы с очень объемными программными системами, включающими более 3500 исходных файлов, что потребовало внедрения порядка 77000 тестовых проб. Анализ испытаний показал довольно неплохие результаты: перекрытие тестовых проб в узких местах составило около 50%, а в среднем по всему приложению не превысило 15%.


***


Тестовое покрытие кода является важной мерой оценки качества разрабатываемых программных систем.


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


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


  • Полный набор определений конкретного обрабатываемого языка кодирования.

  • Написание небольшого набора правил для преобразований исходного кода (по принципу source-to-source).

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


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


Заключение


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


Рассмотрены функциональные возможности системы программного реинжиниринга DMS Software Reengineering Toolkit. Перечислены основные достоинства системы, в числе которых поддержка разностороннего усовершенствования встраиваемых программных приложений, имеющих промышленное применение. Поддерживается обработка кода приложений, написанного на большинстве известных языков кодирования и работающих практически на любых программных платформах.


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


Приведены практические примеры и результаты производственных испытаний работы системы DMS.


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


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


Литература


[1] А. Сергеева. Тестирование работоспособности промышленного компьютера. // Компоненты и технологии. 2014. № 2.

[2] А. Сергеева. Одновременная разработка программ и аппаратуры для встраиваемых систем при помощи симулятора аппаратуры Vista Virtual Prototyping. // Компоненты и технологии. 2014. № 3.

[3] Chikofsky, E. и Cross, J.// Reverse Engineering and Design Recovery: A Taxonomy. — IEEE Software, 1990.

[4] Сайт компании Reasoning Systems — http://www.reasoning.com/

[5] Сайт компании Stratego — http://strategoxt.org/

[6] Сайт компании Semantic Designs – http://www.semdesigns.com/

[7] Об инструментарии DMS Software Reengineering Toolkit — http://www.semdesigns.com/products/DMS/DMSToolkit.html

[8] О проекте JOVIAL2C — http://www.tgdaily.com/technology/42426-us-upgrades-stealth-bomber-software