Предисловие Этот специальный выпуск трудов Института системного программирования РАН составлен из статей сотрудников отдела технологий программирования и посвящен различным методам тестирования сложных программных и аппаратных систем. Хотя в настоящий момент разрабатываемые в отделе технологии тестирования очень сильно различаются в силу различий области применения, задач и окружения проектов, для которых они предназначены, нашей целью является создание целостного подхода к тестированию сложных систем, унифицирующего методы тестирования в достаточно широкой области и предназначенного для применения в разнообразных ситуациях. Первые две работы второй части сборника, статья А. С. Камкина и М. М. Чупилко «Тестирование модулей арифметики с плавающей точкой микропроцессоров на соответствие стандарту IEEE 754» и статья А. С. Камкина «Генерация тестовых программ для микропроцессоров» посвящены методам тестирования аппаратного обеспечения, разрабатываемым в ИСП РАН. Первая статья рассказывает о тестировании модулей вычислений с плавающей точкой, использующем технологию UniTESK и специальные техники выбора тестов данных для проверки корректности выполнения операций над числами с плавающей точкой. Вторая статья представляет методику автоматической генерации тестовых программ для системного тестирования микропроцессоров, основанную на комбинаторном переборе сочетаний различных условий выполнения отдельных инструкций.
А. В. Пономаренко, В. В. Рубанова и Е. С. Чернова «Технология Azov автоматизации массового создания тестов работоспособности» представляет технологию Azov в целом и сопоставляет ее с аналогами. Статья Р. С. Зыбина, А. В. Пономаренко, В. В. Рубанова и Е. С. Чернова «Расширение описаний сигнатур операций для автоматической генерации тестов» рассказывает более детально о ключевом элементе технологии Azov — специализированных типах, и о методике их создания. Третья статья А. В. Пономаренко и Е. С. Чернова «Алгоритм генерации тестов работоспособности на основе расширенной базы данных LSB» содержит описание алгоритма автоматической генерации тестов, используемого в технологии Azov. Последняя в этой серии статья «Опыт применения технологии Azov для тестирования библиотеки Qt3» Р. С. Зыбина анализирует особенности использования этой технологии при разработке тестов работоспособности для переносимой библиотеки Qt версии 3, предназначенной для создания приложений с графическим пользовательским интерфейсом. Последняя статья сборника «Автоматизация тестирования web-приложений, основанных на скриптовых языках» Д. В. Силакова описывает набор техник, которые можно использовать для автоматизации тестирования Webприложений, большая часть функций которых реализована с помощью скриптов, кода на интерпретируемых языках. В этой статье также представлены результаты тестирования с помощью перечисленных техник LSB Навигатора, приложения, предназначенного для представления и анализа информации о различных составляющих стандарта Linux Standard Base. Член-корреспондент РАН В. П. Иванников Кандидат физ.-мат. наук В. В. Кулямин
Статья В. В. Рубанова, А. В. Хорошилова и Е. А. Шатохина «T2C: технология автоматизированной разработки тестов базовой функциональности программных интерфейсов» описывает разработанную в ИСП РАН технологию T2C (Template-to-Code), повышающую эффективность ручной разработки тестов базовой функциональности и обеспечивающую строгую привязку всех создаваемых тестов к требованиям к тестируемой системе. Четыре статьи, идущие следом, посвящены одной теме — новой технологии Azov, предназначенной для автоматической генерации тестов, проверяющих минимальную работоспособность тестируемой системы, на основе базы данных, содержащей структурированную информацию об интерфейсных операциях этой системы. Статья Р. С. Зыбина, В. В. Кулямина, 5
6
Тестирование модулей арифметики с плавающей точкой микропроцессоров на соответствие стандарту IEEE 754 А. C. Камкин, М. M. Чупилко {kamkin, chupilko}@ispras.ru Аннотация. В работе рассматривается методика функционального тестирования модулей арифметики с плавающей точкой микропроцессоров (FPUs, Floating Point Units) на соответствие стандарту IEEE 754. Методика основана на технологии тестирования UniTESK, но дополнена методами генерации тестов для операций над числами с плавающей точкой. Большое внимание в статье уделяется операциям деления и извлечения квадратного корня. Важной частью работы является описание опыта применения предлагаемой методики.
1. Введение Модули арифметики с плавающей точкой (FPUs, Floating Point Units) являются важной составной частью микропроцессоров. Они используются в научно-технических расчетах и задачах обработки мультимедиа-данных, без них редко обходятся бортовые системы самолетов и космических спутников, они входят в состав станков с программным управлением и медицинского оборудования. Ошибки в таких системах могут стоить жизней или здоровья людей, поэтому не удивительно, что к модулям арифметики предъявляют очень строгие требования. Большое влияние на формирование жестких требований оказывают также экономические факторы. В отличие от программного обеспечения, в котором исправление ошибки стоит сравнительно дешево, ошибка в аппаратном обеспечении, обнаруженная несвоевременно, может потребовать перевыпуск и замену продукции, а это сопряжено с очень высокими затратами. Так, известная ошибка в реализации инструкции деления чисел с плавающей точкой микропроцессора Pentium1 [1] (FDIV bug) обошлась компании Intel в 475 миллионов долларов [2,3].
Модули арифметики, как и любая цифровая аппаратура, разрабатываются на специальных языках описания аппаратуры (HDLs, Hardware Description Languages), например, Verilog или VHDL [4]. Такие языки позволяют описать функциональность электронной схемы, абстрагируясь от деталей расположения и соединения ее элементов. Использование языков описания аппаратуры значительно повышает продуктивность разработки аппаратуры, но не страхует от ошибок, поэтому функциональное тестирование попрежнему остается актуальной и востребованной задачей. Формат и правила действий над числами с плавающей точкой определены в стандарте IEEE 754 [5] (он же — IEC 60559 [6]). Стандарт описывает операции сложения, умножения, вычитания, деления, вычисления остатка от деления, извлечения квадратного корня и преобразований между различными типами чисел — это базовый набор операций, реализуемых в FPU микропроцессоров. Для всех операций требуется, чтобы их результат получался из точного путем приведения к представимому числу согласно установленному режиму округления. В идеале, чтобы убедиться в том, что операция над числами с плавающей точкой реализована корректно, необходимо проверить ее результат на всех возможных значениях операндов. Огромное пространство входных данных не позволяет это сделать за разумное время2, поэтому необходимы методики построения тестов, которые при приемлемом размере тестового набора обеспечивают высокое качество тестирования. В работе рассматривается методика построения тестов для FPU микропроцессоров, которая основана на технологии тестирования UniTESK [7], использующей формальные спецификации требований к системе для автоматизированного построения тестов на соответствие им [8]. Основные элементы технологии UniTESK применительно к модулям арифметики таковы. Требования на тестируемые операции представляются формально в виде эталонных реализаций. Чтобы сделать спецификации независимыми от конкретного интерфейса модуля, разрабатывается прослойка медиаторов, связывающих спецификации операций с тестируемым модулем. Рассматриваемая методика дополняет технологию UniTESK методами генерации тестов для операций над числами с плавающей точкой. В рамках предлагаемого подхода используются несколько типов тестов: сложные случаи округления, тесты на исключительные ситуации, особые случаи (специальные значения операндов, граничные значения и другие) и тесты с определенной битовой структурой операндов. Подход был успешно апробирован в проектах по тестированию Verilog-моделей FPU, реализующих операции деления и извлечения квадратного корня. 2
1
Pentium — торговая марка нескольких поколений микропроцессоров семейства x86, выпускаемых компанией Intel с 22 марта 1993 года. 7
Исключение составляют унарные операции над числами однократной точности. (Определение чисел однократной точности дается во втором разделе.) 8
Статья состоит из шести разделов, включая введение и заключение. Второй раздел посвящен стандарту IEEE 754. В этом разделе описываются основные положения стандарта касательно представления чисел с плавающей точкой, режимов округления и исключений, вызываемых операциями. В третьем разделе описаны предлагаемые методики построения тестов. Четвертый раздел описывает архитектуру разработанной тестовой системы, предназначенной для тестирования Verilog-моделей FPU. В пятом разделе приведены результаты апробации тестовой системы.
2. Обзор стандарта IEEE 754 Стандарт IEEE 754 определяет представление двоичных чисел с плавающей точкой, а также режимы округления операций и исключения — специальные флаги, сигнализирующие либо о некорректности значений операндов, либо об особенностях результата операции. В дальнейшем будем называть двоичные числа с плавающей точкой просто числами с плавающей точкой, а действительные числа, точно представимые в виде чисел с плавающей точкой — представимыми.
Значения NaN подразделяются на тихие (QNAN, Quiet NaN) и сигнальные (SNaN, Signaling NaN), которые отличаются старшим битом мантиссы: если он равен единице, это QNaN; в противном случае — SNaN. Стандарт IEEE 754 определяет несколько возможных типов чисел с плавающей точкой (отличающихся числом бит, отводимых под порядок и мантиссу), среди которых чаще всего используются числа однократной точности (singles, single precision numbers) и числа двойной точности (doubles, double precision numbers). Числа однократной точности — это числа с плавающей точкой, для представления которых используется 32 бита: 1 бит на знак, 8 бит на порядок и 23 бита на мантиссу. знак
2.1. Представление чисел с плавающей точкой
порядок
мантисса
Рис. 1. Представление числа однократной точности.
Согласно стандарту IEEE 754, битовое представление чисел с плавающей точкой содержит три составляющие: знак, порядок и мантиссу. Под знак отводится один бит (значение 0 соответствует положительным числам, 1 — отрицательным); число бит, отводимое под порядок и мантиссу, зависит от типа чисел. Пусть под порядок отводится n бит. Число B = 2n–1 – 1 называется смещением порядка (bias). Знаковый бит S, порядок E и мантисса M числа x определяют его значение по следующим правилам: x = (–1)S · 2e · m, где: если E 0, то e = E – B; иначе, e = –B + 1; если 0 < E < 2n – 1, то m имеет двоичное представление 1.M, то есть целая часть m равна 1, а последовательность цифр дробной части совпадает с последовательностью бит M; если E = 0, то m имеет двоичное представление 0.M, такие числа (с нулевым порядком) называются денормализованными. Заметим, что среди чисел с плавающей точкой существует число –0, которое стандарт требует считать равным 0. Стандартом определены особые комбинации значений порядка и мантиссы: бесконечности () — все разряды порядка равны единице, мантисса равна нулю (E = 2n – 1, M = 0); не-число (NaN, Not-a-Number) — все разряды порядка равны единице, мантисса отлична от нуля (E = 2n – 1, M 0). 9
Числа двойной точности — это числа с плавающей точкой, для представления которых используется 64 бита: 1 бит на знак, 11 бит на порядок и 52 бита на мантиссу.
2.2. Режимы округления Стандарт IEEE 754 описывает операции сложения, умножения, вычитания, деления, вычисления остатка от деления, извлечения квадратного корня и преобразований между различными типами чисел. Общий принцип всех операций заключается в том, что результат получается из точного путем приведения к представимому числу согласно установленному режиму округления. В стандарте определены четыре режима округления: округление к ближайшему представимому числу; округление к –; округление к +; округление к 0. В режиме округления к ближайшему представимому числу, как следует из названия, результатом операции является представимое число, ближайшее к точному значению. Когда точное значение одинаково удалено от двух представимых чисел, выбирается то, у которого младший бит мантиссы равен нулю. При округлении к – результатом является ближайшее представимое число, не превосходящее точного значения; при округлении к + — ближайшее 10
представимое число, которое не меньше точного значения; при округлении к 0 — ближайшее представимое число, не превосходящее по абсолютной величине точного значения. Три последних режима называют режимами направленного округления (directed rounding). Для иллюстрации режимов округления рассмотрим примеры. В первом примере требуется округлить число к ближайшему представимому числу однократной точности:
исходная мантисса: 101010101010101010101010111111112 округленная мантисса: 101010101010101010101010000000002
бесконечности делимое. В случае возникновения исключения division by zero результат равен бесконечности: (1.797693e+308) делимое: 7FEFFFFFFFFFFFFF16 делитель: 000000000000000016 (+0) частное: 7FF000000000000016 (+) исключения: division by zero Исключение overflow возникает, когда результат операции превышает (по абсолютной величине) наибольшее представимое число: делимое: 7FEFFFFFFFFFFFFF16 (1.797693e+308) делитель: 0003A6B50B0F27BB16 (5.077619e–309) частное: 7FEFFFFFFFFFFFFF16 (1.797693e+308) исключения: overflow и inexact
В следующем примере требуется округлить число с заданной мантиссой в сторону 0: исходная мантисса: 101010101010101010101011111111112 округленная мантисса: 101010101010101010101010000000002
Исключение underflow возникает, когда результат операции настолько мал (по абсолютной величине), что он может быть представлен только денормализованным числом: делимое: 001000000000000016 (2.225074e–308) делитель: 7FEFFFFFFFFFFFFF (1.797693e+308) 16 частное: 000000000000000016 (+0) исключения: underflow и inexact
2.3. Исключения
Исключение inexact возникает, когда результат операции отличается от точного значения: делимое: 27E83F0F3FFC953816 (1.786431e+000) делитель: FBAF43813FFFFFFF (2.000000e+000) 16 частное: 2BC3037F3FEC953816 (8.932153e–001) исключения: inexact
Стандартом IEEE 754 определены пять типов исключений: invalid operation — некорректная операция; division by zero — деление на нуль; overflow — переполнение; underflow — потеря значимости; inexact — потеря точности. В стандарте приводится список ситуаций, в которых выставляется исключение invalid operation. Ниже представлены некоторые из них: любые операции над SNaN; сложение бесконечностей с разными знаками или вычитание бесконечностей с одинаковыми знаками; умножение 0·∞; деление 0/0 или ∞/∞; взятие остатка от деления x rem y, где y = 0 или x = ; извлечение квадратного корня из отрицательного числа. Рассмотрим остальные исключения на примере деления чисел двойной точности в режиме округления к 0. Исключение division by zero выставляется, когда делитель равен нулю. Обязательное условие возникновения исключения — не равное нулю или 11
3. Методика построения тестовых данных Для качественного тестирования модулей арифметики с плавающей точкой тесты должны затрагивать различные аспекты их функциональности: выполнение операций, округление результата, выставление исключений и так далее. В связи с этим мы используем несколько типов тестов: сложные случаи округления, тесты на исключительные ситуации, особые случаи (специальные значения операндов, граничные значения и другие) и тесты с определенной битовой структурой операндов. Помимо перечисленных типов тестов мы также используем случайные тесты, которые бывают полезны на ранних стадиях тестирования.
3.1. Сложные случаи округления Сложным случаем округления (hard-to-round case, extremal rounding boundary case) называется ситуация, когда значения операндов таковы, что результат операции очень близок к числу, представимому в форме числа с плавающей 12
точкой заданной точности, но отличается от него на величину значительно меньшую единицы последнего разряда мантиссы (ulp, unit in the last place). Очевидно, что значения операндов для сложных случаев округления зависят как от операции, так и от режима округления. В сложных случаях округления возникает так называемая дилемма составителя таблиц (table maker’s dilemma). Дилемма состоит в том, что для выбора правильно округленного числа с плавающей точкой следует вычислить много дополнительных бит мантиссы результата, значительно больше, чем имеется в рассматриваемом типе чисел с плавающей точкой [8]. Для более точного определения сложных случаев округления введем понятие остаточных бит результата. Под остаточными битами будем понимать последовательность битов точно вычисленного результата, начинающуюся с бита, следующего за младшим битом мантиссы. В общем случае последовательность остаточных бит может быть бесконечной. Сложным случаем для режима округления к ближайшему представимому числу (RN-hard, hard to round to nearest) называется ситуация, когда остаточные биты результата начинаются на 100...0 или 011...1, где число нулей или, соответственно, единиц в конце последовательности достаточно велико. Сложным случаем для режима направленного округления (RD-hard, hard to round for directed rounding) называется ситуация, когда остаточные биты результата начинаются на 00...01 или 11...10, где число нулей или, соответственно, единиц в начале последовательности достаточно велико. Построению сложных случаев округления для различных операций, режимов и точности посвящено большое число исследований [9-16]. Используемый нами метод генерации таких чисел для операции деления основан на работах [9-14]. Для операции извлечения квадратного корня мы использовали метод, описанный в работе [15]. Важно отметить, что тесты на сложные случаи округления для операции деления (по крайней мере, для режимов округления к ) должны содержать как ситуации, в которых операнды имеют одинаковый знак, так и ситуации, в которых знаки операндов различны, поскольку знак результата (в этих режимах) влияет на округление. Такого рода ошибка была обнаружена нами в одной из реализаций FPU. Более подробное описание этой ошибки содержится в разделе “Практическая апробация подхода”.
включать тесты, в которых создаются ситуации “близкие” к исключительным, но таковыми не являющиеся. Заметим, что для проверки правильности выставления исключения inexact можно использовать тесты на сложные случаи округления. Остановимся на операциях деления и извлечения квадратного корня. В силу своей природы операция извлечения квадратного корня никогда не вызывает исключений overflow и underflow. Тесты на исключение division by zero разрабатываются очевидным образом (они должны содержать деление конечных ненулевых чисел на 0). Рассмотрим подробнее тесты на исключение invalid operation (для обеих операций) и тесты на исключения overflow и underflow (для операции деления). IEEE 754 требует, чтобы любая операция, использующая в качестве хотя бы одного операнда значение SNaN вызывала исключение invalid operation. Использование в качестве операндов SNaN и других особых значений реализуется в так называемых тестах на особые значения, которые будут описаны в соответствующем разделе. Операция деления вызывает исключение invalid operation в случае неопределенности вида 0/0 или ∞/∞. Тесты для операции деления должны включать обе неопределенности с различными знаками операндов. Операция извлечения квадратного корня вызывает исключение invalid operation в случае, если операнд является отрицательным числом (исключение составляет значение –0, для которого значение квадратного корня определено и равно –0). Тесты для операции извлечения квадратного корня должны включать отрицательные числа, включая –0. Пусть M1 и M2 — мантиссы первого и второго операндов, E1 и E2 — их порядки. Обозначим через B смещение, а через Emax — максимальное значение порядка нормализованного числа3. Для наглядности рассмотрим случай, когда оба операнда являются нормализованными числами. Операция деления вызывает исключение overflow, если выполнено одно из следующих условий:
M1 < M2 B + (E1 – E2) – 1 > Emax: Если M1 < M2, результат деления мантисс лежит в интервале (0.5, 1). В этом случае для нормализации результата необходимо удвоить мантиссу и уменьшить порядок на единицу. Отсюда следует, что исключение возникает, если выполнено условие B + (E1 – E2) – 1 > Emax.
3.2. Исключительные ситуации Как было сказано выше, стандарт IEEE 754 определяет пять типов исключений. Не каждая операция вызывает все перечисленные исключения, однако, если тестируемая операция может вызвать некоторое исключение, тесты, в которых соответствующие ситуации реализуются, должны обязательно входить в тестовый набор. Также в тестовый набор целесообразно 13
3
Значения B и Emax зависят от точности рассматриваемого типа чисел с плавающей точкой: для чисел ординарной точности В равно 7F16=127, Emax — FE16=254; для чисел двойной точности — В равно 3FF16=1023, Emax — 7FE16=2046. 14
M1 M2 B + (E1 – E2) > Emax: Если M1 M2, результат деления мантисс лежит в полуинтервале [1, 2). В этом случае нормализация результата не требуется, поэтому условие на исключение имеет вид B + (E1 – E2) > Emax.
Пусть, как и прежде, M1, M2 — мантиссы первого и второго операндов, E1 и E2 — их порядки, B — смещение. Обозначим через Emin минимальное значение порядка нормализованного числа4. Для наглядности рассмотрим случай, когда оба операнда являются нормализованными числами. Исключение underflow возникает, если выполнено одно из следующих условий:
M1 < M2 B + (E1 – E2) – 1 < Emin; M1 M2 B + (E1 – E2) < Emin.
Тесты должны включать ситуации как первого, так и второго типа, а также ситуации “близкие” к ним. Такие ситуации могут быть получены ослаблением приведенных выше условий. Так, для исключения overflow можно использовать значение Emax, уменьшенное на единицу, а для исключения underflow — значение Emin, увеличенное на единицу.
3.3. Особые случаи Под особыми случаями мы понимаем числа с плавающей точкой, которые имеют специальную, определенную стандартом IEEE 754 семантику (, NaN), либо числа, в которых порядок и/или мантисса имеют граничные значения. Основные особые случаи для чисел однократной и двойной точности представлены в Таблице 1. Граничные значения: наименьшие (по абсолютной величине) представимые числа и наибольшие (по абсолютной величине) представимые числа приведены в Таблице 2.
3.4. Числа с определенной битовой структурой Использование чисел с определенной битовой структурой нацелено на тестирование следующих особенностей FPU микропроцессоров. Во-первых, для реализации операций над числами с плавающей точкой часто применяются табличные алгоритмы. Такие алгоритмы используют таблицу начальных приближений результата операции, которая индексируется с помощью определенных битов операндов. Поскольку таблица начальных приближений может содержать ошибочные данные, необходимо чтобы тесты покрывали все элементы этой таблицы. Во-вторых, результат операции часто вычисляется не полностью, а составляется из небольших частей путем сложения, конкатенации или с помощью других простых операций. В этом 4
Emin всегда равно 1 независимо от точности рассматриваемого типа чисел с плавающей точкой. 15
случае необходимо проверить правильность вычисления правильность составления из них окончательного результата. +0 –0 денорм. Числа + – SNaN QNaN
знак 0 1 {0,1 } 0 1
частей
однократная точность порядок мантисса 0016 0 0016 0
знак 0 1
0016
любая ≠ 0
{0,1}
00016
любая ≠ 0
FF16 FF16
0 0 любая ≠ 0, старший бит =0 любая ≠ 0, старший бит =1
0 1
7FF16 7FF16
0 0
{0,1}
7FF16
любая ≠ 0, старший бит =0
{0,1}
7FF16
любая ≠ 0, старший бит =1
{0,1 }
FF16
{0,1 }
FF16
и
двойная точность порядок мантисса 00016 0 00016 0
Таблица 1. Основные особые случаи. Рассмотрим известную ошибку в FPU микропроцессора Pentium (1994 г.), связанную с неправильными данными в таблице начальных приближений результата [1-3]. Тим Коу (Tim Coe) и Пинг Так Петер Танг (Ping Tak Peter Tang), а также Алан Эдельман (Alan Edelman) провели детальный анализ этой ошибки [17,18]. Они определили, что ошибка возникает, только в том случае, если мантисса делителя имеет вид M = m1m2 …, где биты c 5-го по 10-ый включительно равны единице. Более того, биты m1m2 … m4 при этом должны принимать одно из пяти определенных значений. Вероятность возникновения подобных ошибок на случайных тестах очень мала. Более того, такие ошибки можно не обнаружить, используя тесты на сложные случаи округления или другие типы тестов, описанные выше. знак наибольшее норм. Число наименьшее норм. Число Наибольшее денорм. Число Наименьшее денорм. Число
однократная точность порядок мантисса
знак
двойная точность порядок мантисса
{0,1}
FE16
7FFFFF16
{0,1}
7FE16
FF…F16
{0,1}
0116
00000016
{0,1}
00116
00...016
{0,1}
0016
7FFFFF16
{0,1}
00016
FF…F16
{0,1}
0016
00000116
{0,1}
00016
00...116
Таблица 2. Наибольшие и наименьшие представимые числа. В идеале, тестирование должно учитывать алгоритмы и схемы, использованные для реализации тестируемых операций. Если по какимнибудь причинам это сделать не удается, например, когда не доступны исходные коды модуля и документация к нему, для перебора значений 16
операндов (или только их мантисс) можно использовать следующую эвристику. В операндах полностью перебираются только N последовательных бит, значения остальных бит при этом остаются фиксированными. После полного перебора положение бит меняется. Процесс продолжается до тех пор, пока не будут использованы все возможные положения перебираемых бит. Число N должно быть не слишком маленьким, чтобы обеспечить приемлемое качество тестирования, и не слишком большим, чтобы позволить перебрать операнды (и их комбинации, если операндов несколько) за разумное время. Подобную эвристику можно использовать и применительно к битам результата. В этом случае, чтобы получить значения операндов необходимо разрешить уравнение f(x1, …, xn) = y, где f — тестируемая операция, y — требуемое значение результата, а x1, …, xn — искомые значения операндов. 11111111...11111111 11011110...10101101 11111111...11111111 фиксированные фиксированные перебираемые биты биты биты Рис. 2. Схема построения чисел с определенной битовой структурой. Следует отметить, что использование таких простых эвристик позволило нам обнаружить и локализовать критическую ошибку в операции вычисления квадратного корня для чисел двойной точности в FPU промышленного микропроцессора. Более подробное описание этой ошибки содержится в разделе “Практическая апробация подхода”.
Симулятор
Verilog-окружение (testbench)
VPI-медиатор
Тестовая система Генератор тестовых воздействий
Параметры теста
Verilog-модель FPU
Тестовый оракул
Медиатор
велась на языке программирования C. В качестве основы построения тестовой системы мы использовали технологию UniTESK [7,19]. Архитектура тестовой системы показана на Рис. 3. Verilog-окружение (testbench) содержит экземпляр тестируемой Verilogмодели FPU. В начале тестирования окружение инициализирует тестируемую модель, после чего в цикле принимает от тестовой системы тестовые воздействия (код операции и значения операндов), подает их на тестируемую модель, ожидает реакции (результат операции и флаги исключений) и передает их тестовой системе для проверки. Прием тестовых воздействий и передача реакций осуществляются через VPI-медиатор. VPI-медиатор связывает тестируемую модель с медиатором тестовой системы. Он реализует установку значений входных сигналов тестируемой модели в соответствии текущим тестовым воздействием, а также съем значений выходных сигналов. VPI-медиатор реализован с помощью интерфейса VPI (Verilog Procedural Interface) [20]. Медиатор преобразует значения операндов операции из внутреннего представления тестовой системы в представление, описанное в стандарте, и наоборот — значение результата операции из стандартного представления во внутреннее. Тестовый оракул оценивает правильность поведения тестируемой модели в ответ на единичное тестовое воздействие. В основе тестового оракула лежат эталонные реализации операций над числами с плавающей точкой. Тестовый оракул записывает в трассу теста тестовые воздействия, на которых тестируемая модель FPU выдает ошибочный результат (результат, расходящийся с эталонной реализацией). Генератор тестовых воздействий реализует построение тестов описанных выше типов: сложные случаи округления, тесты на исключительные ситуации, особые случаи, тесты с определенной битовой структурой операндов и случайные тесты. Выбор того или иного сценария генерации осуществляется на основе параметров теста, которые также задают точность чисел, режим округления и ограничения на число тестов.
4.1. Эталонные реализации операций Для эталонной реализации операций используется расширенное представление чисел с плавающей точкой, в котором число бит, отводимое под мантиссу, достаточно для корректного округления результата реализуемых операций5.
Трасса теста
Рис. 3. Архитектура тестовой системы.
4. Архитектура тестовой системы
5
В соответствии с описанным подходом нами была разработана тестовая система для тестирования Verilog-моделей FPU микропроцессоров. Разработка 17
Для корректного округления результата сложения, вычитания, умножения, деления и извлечения квадратного корня достаточно, чтобы размер расширенного представления мантиссы был равен 2 |M|, где |M| — это число 18
Эталонные реализации операций над числами с плавающей точкой описывают формально требования стандарта IEEE 754. Они вычисляют корректные результаты операций (с учетом установленного режима округления) и устанавливают флаги исключений. Рассмотрим кратко используемые нами эталонные реализации операций деления и извлечения квадратного корня без учета обработки исключительных ситуаций.
4.1.1. Эталонная реализация деления Эталонная реализация операции деления основана на известном алгоритме деления двоичных чисел с фиксированной точкой. Перед использованием алгоритма вычисляется знак результата (исключающее ИЛИ знаков операндов) и порядок (разность порядков делимого и делителя). После этого операнды приводятся к специальному виду 01M, где M — это биты мантиссы. Полученные значения операндов заносятся в переменные N (делимое) и D (делитель). Значения старших бит переменных будем называть знаками. Вычисление происходит последовательно от старшего к младшему биту мантиссы результата: если знак N равен нулю, в текущий разряд результата записывается единица, иначе — нуль; если знак N равен единице, N присваивается результат сложения N и D; иначе — результат сложения N и дополнения D; значение N сдвигается на один разряд влево. Полученный результат преобразуется в форму мантиссы, для этого он сдвигается влево. Величиной сдвига является минимальное число, при котором старший единичный разряд оказывается за пределами разрядной сетки мантиссы. Заметим, что при сдвиге необходимо корректировать порядок результата.
алгоритма, в основе которого лежит возведение в квадрат. Последовательно от старшего к младшему биту мантиссы производятся следующие действия: текущий бит мантиссы результата устанавливается в единицу; полученная мантисса возводится в квадрат и сравнивается с исходным числом; если исходное число меньше, установленная единица сбрасывается в нуль.
5. Практическая апробация подхода Разработанная тестовая система была успешно применена для тестировании двух различных Verilog-моделей FPU с одинаковым интерфейсом (будем обозначать их FPU1 и FPU2), реализующих операции деления и извлечения квадратного корня для чисел однократной и двойной точности. В результате тестирования FPU1 была найдена критическая ошибка в реализации операции извлечения квадратного корня для чисел двойной точности. Ошибка была обнаружена на тестах с определенной битовой структурой операнда. Такие тесты позволили нам найти значения, на которых операция возвращала ошибочный результат. Анализ обнаруженных значений показал — если извлечь из них квадратный корень, то мантиссы результата имеют определенную структуру: младшие 36 бит являются единичными, а 16 старших — “случайные”. Мы перебрали все числа с описанной структурой (при некотором фиксированном порядке) — числа возводились в квадрат и подавались в качестве операнда. В результате было найдено большое число (около 6000 для каждого фиксированного порядка) “ошибочных” значений. Рассмотрим примеры (округление осуществляется в сторону 0): операнд: 64300800FFFFFFFE16 полученный результат: 52100403FFFFFFFF16 корректный результат: 521003FFFFFFFFFF16
4.1.2. Эталонная реализация извлечения квадратного корня Извлечение квадратного корня реализовано следующим образом. Знак результата совпадает со знаком операнда (отрицательный знак операнда возможен только в случае, когда его значение равно –0), порядок равен половине порядка операнда6. Мантисса результата вычисляется с помощью бит, отводимое под мантиссу в рассматриваемом типе чисел с плавающей точкой [21]. 6 Если быть точнее, используются следующие правила вычислений: если E < B, то Eres B – ((B – E) >> 1) o если Е mod 2 = 0, то Eres Eres – 1 если E B, то Eres B + ((E – B) >> 1) 19
(3.965019e+174) (1.991243e+087) (1.991236e+087)
операнд: 2CB01687E8FFFFFE16 (1.928163e–093) полученный результат: 36500B43FFFFFFFF16 (4,391102e–047) корректный результат: 36500B3FFFFFFFFF16 (4.391085e–047) В результате тестирования FPU2 были найдены ошибки, связанные с неточностями в округлении и выставлении флагов исключений. Например, была обнаружена ошибка, возникающая при округлении к + отрицательного результата (ошибка проявлялась как на числах однократной точности, так и двойной).
20
Рассмотрим примеры: делимое: делитель: полученный результат: корректный результат:
68CDCD2C16 A8B5F04C16 FF80000016 FF7FFFFF16
(7.774959e+024) (–2.019925e–014) (–) (–3.402823e+038)
делимое: делитель: полученный результат: корректный результат:
983FFFFFBD72729216 581000007B4947AD16 801FFFFEC6DFECA516 801FFFFEC6DFECA416
(–7.013789e–192) (1.576081e+116) (–4.450145e–308) (–4.450145e–308)
Тестовая система также использовалась для тестирования Verilog-модели модуля деления чисел однократной точности, доступной на сайте [22]. Следует отметить, что адаптация тестовой системы под эту реализацию заняла около 30 минут. Изменения касались компонентов тестовой системы наиболее приближенных к тестируемой модели, а именно Verilog-окружения и VPIмедиатора (это около 10% от общего объема исходного кода тестовой системы). Ошибок обнаружено не было.
6. Заключение В работе была рассмотрена методика функционального тестирования модулей арифметики с плавающей точкой микропроцессоров на соответствие стандарту IEEE 754. Методика основана на технологии тестирования UniTESK, но дополнена методами генерации тестов для операций над числами с плавающей точкой, которые позволяют осуществить разностороннее систематичное тестирование FPU. Разработанная согласно описанной методике тестовая система была использована для тестирования нескольких Verilog-моделей FPU, реализующих операции деления и извлечения квадратного корня для чисел однократной и двойной точности. В результате тестирования было найдено несколько серьезных ошибок.
[9] D. Matula, L. McFearin. Number Theoretic Foundations of Binary Floating Point Division with Rounding. Real Numbers and Computers, 2000. [10] D. Matula, L. McFearin. Generation and Analysis of Hard to Round Cases for Binary Floating Point Division. Symposium on Computer Arithmetic, 2001. [11] D. Matula, L. McFearin. Selecting a Well Distributed Hard Case Test Suite for IEEE Standard Floating Point Division. International Conference on Computer Design, 2001. [12] D. Matula, L. McFearin. A p×p Bit Fraction Model of Binary Floating Point Division and Extremal Rounding Cases. Theoretical Computer Science, 2003. [13] D. Matula, L. McFearin. A Formal Model and Efficient Traversal Algorithm for Generating Testbenches for Verification of IEEE Standard Floating Point Division. Conference on Design, Automation and Test in Europe, 2006. [14] D. Matula, L. McFearin. Generating a Benchmark Set of Extremal Rounding Boundary Instances for IEEE Standard Floating Point Division. http://engr.smu.edu/~matula/single/paper.ps. [15] W. Kahan. A Test for Correctly Rounded Square Root. Computer Science Department, Berkeley, 1994. [16] M. Parks. Number-Theoretic Test Generation for Directed Rounding. IEEE Transactions on Computers, Vol. 49. 2000. [17] T. Coe, P. T. P. Tang. It Takes Six Ones to Reach a Flaw. Chinese University of Hong Kong. Technical Report 95-5(61), 1995. [18] A. Edelman. The Mathematics of the Pentium Division Bug. 1995. http://www-math.mit.edu/~edelman/homepage/papers/pentiumbug.ps. [19] В. П. Иванников, А. С. Камкин, В. В. Кулямин, А. К. Петренко. Применение технологии UniTesK для функционального тестирования моделей аппаратного обеспечения. Препринт Института системного программирования РАН, 2005. http://citforum.ru/SE/testing/unitesk_hard/. [20] S. Sutherland. The Verilog PLI Handbook: A User's Guide and Comprehensive Reference on the Verilog Programming Language Interface. Springer, 2002. [21] D. Goldberg. What Every Computer Scientist Should Know About Floating-Point Arithmetic. Computing Surveys. March, 1991. [22] http://www.opencores.org.
Литература [1] Statistical Analysis of Floating Point Flaw in the Pentium Processor. Intel Corporation, November 1994. [2] B. Beizer. The Pentium Bug — An Industry Watershed. Testing Techniques Newsletter (TTN), TTN Online Edition, September 1995. [3] А. Wolfe. For Intel, It’s a Case of FPU All Over Again. EE Times, May 1997. [4] А.К. Поляков. Языки VHDL и VERILOG в проектировании цифровой аппаратуры. — М.: СОЛОН-Пресс, 2003. [5] IEEE Standart for Binary Floating-Point Arithmetic 754, 1985. [6] IEC 60559:1989. Binary Floating-Point Arithmetic for Microprocessor Systems. Geneve: ISO, 1989. [7] http://www.unitesk.com. [8] В.В. Кулямин. Формальные подходы к тестированию математических функций. ИСП РАН, 2006.
21
22
Генерация тестовых программ для микропроцессоров А. С. Камкин
[email protected] Аннотация. В работе описывается подход к автоматической генерации тестовых программ, предназначенный для систематичного функционального тестирования микропроцессоров. Предлагаемый подход дополняет такие широко распространенные на практике методы, как тестирование с помощью существующего программного обеспечения и тестирование с помощью случайных программ. Генерация тестовых программ осуществляется на основе модели микропроцессора, включающей в себя структурную модель микропроцессора и модель системы команд. Цель генерации задается с помощью критерия тестового покрытия, выделяющего набор тестовых ситуаций для каждой инструкции микропроцессора. Помимо описания методики генерации тестовых программ в статье также описывается устройство генератора, реализующего эту методику, и опыт его использования для тестирования микропроцессоров.
1. Введение Микропроцессоры являются основой любой компьютерной системы, поэтому от правильности их работы зависит корректность и надежность системы в целом. С ростом использования компьютеров в нашей жизни растет и ответственность разработчиков за качество создаваемых систем. Научнотехнические расчеты, военное дело, управление транспортом, медицинские системы — вот далеко не полный перечень приложений микропроцессорных систем, в которых ошибка может иметь тяжелые последствия. Даже если ошибка не представляет угрозы для жизни или здоровья людей, она может иметь очень тяжелые экономические последствия для компании, производящей микропроцессор или системы на его основе — ошибки плохо сказываются на имидже компании и конкурентоспособности ее продукции. Современные микропроцессоры являются невероятно сложными системами — они состоят из десятков миллионов транзисторов, объединенных в сотни взаимодействующих модулей и подсистем. Для повышения производительности в микропроцессорах используется множество специальных механизмов: конвейер инструкций, суперскалярное выполнение, предсказание переходов, неблокируемое кэширование и многие другие. Все 23
это делает задачу функционального тестирования микропроцессоров1 чрезвычайно трудоемкой. По различным данным, тестирование микропроцессора занимает около 70-80% от общего объема трудозатрат на его разработку. Поэтому развиваются методы автоматизированного тестирования, которые, с одной стороны, нацелены на повышение качества производимых микропроцессоров, с другой — на сокращение цикла разработки. Одним из основных способов тестирования микропроцессоров является проверка корректности их работы на некотором наборе программ. Программы компилируются, загружаются в память и выполняются микропроцессором; результаты их работы протоколируются и используются для анализа правильности работы микропроцессора. Тестовые программы, то есть программы, которые используются для тестирования, могут быть получены разными способами. Одним из способов является кросс-компиляции существующего программного обеспечения (ПО). Программы, полученные таким образом, не дают гарантий относительно качества тестирования — это объемные тесты, которые широко охватывают функциональность микропроцессора, но не достаточно глубоко. Альтернативой тестам на основе существующего ПО являются программы, специально разработанные (или автоматически сгенерированные) для целей тестирования. Широко распространенным методом автоматического построения тестовых программ является случайная генерация, но этот метод также нельзя назвать систематичным. К таковым мы относим подходы, нацеленные на достижение определенного критерия тестового покрытия и дающие гарантии относительно полноты тестирования с точки зрения этого критерия. Анализ ошибок в микропроцессоре MIPS R4000 PC/SC (ревизия 2.2) [1], проведенный в работе [2], говорит, что большинство ошибок (93.5%) связано с некорректной реализацией управляющей логики (control logic bugs). Управляющей логикой называется функциональность микропроцессора, отвечающая за планирование и организацию процессов выполнения инструкций: конвейер инструкций, суперскалярное выполнение, предсказание ветвлений и так далее. Для обнаружения большей части таких ошибок необходимо одновременно реализовать несколько условий, например, одна из ошибок микропроцессора проявлялась только в следующей ситуации (см. пункты 4 и 14 [1]).
1
Тестируется обычно не сам микропроцессор, а его проектная модель уровня регистровых передач (RTL, Register Transfer Level), разработанная на специализированном языке описания аппаратуры (HDL, Hardware Description Language), например, Verilog или VHDL. Такое тестирование называется имитационным (simulation-based validation), поскольку производится в специальном симуляторе, осуществляющем имитационное моделирование работы микропроцессора. 24
Инструкция загрузки данных в регистр вызывает промах в кэше данных.
За ней через одну инструкцию NOP (No Operation, специальная инструкция микропроцессора, которая не производит никаких действий и обычно используется для временных задержек и выравнивания памяти) следует инструкция безусловного перехода по адресу, содержащемуся в загруженном регистре.
Инструкция перехода — виртуальной памяти.
Номер следующей страницы не содержится в буфере трансляции адресов (TLB, Translation Lookaside Buffer).
последняя
инструкция
на
странице
Заметим, что такие ошибки сложно обнаружить, используя существующее ПО или случайную генерацию, поскольку вероятность появления соответствующей ситуации в тестовой программе очень мала. Предлагаемый в работе подход за счет систематичного перебора и комбинирования тестовых ситуаций для отдельных инструкций позволяет обнаруживать такие ошибки; кроме того, он предоставляет разработчику тестов набор параметров, изменяя которые можно управлять глубиной тестирования. В основу работы положен опыт отдела Технологий программирования Института системного программирования РАН [3,4] разработки тестов и создания автоматических генераторов тестовых данных на основе моделей. Класс ошибок Число ошибок Процент ошибок Ошибки обработки данных 3 6.5% (datapath bugs) Ошибки в управляющей логике / одно условие 17 37.0% (control logic bugs / single event) Ошибки в управляющей логике / несколько условий 26 56.5% (control logic bugs / multiple events) Всего ошибок 46 100.0% Таблица 1. Соотношение разных видов ошибок в микропроцессорах [2]. Оставшаяся часть статьи организована следующим образом. Во втором, следующем за введением, разделе рассказывается о существующих методах построения тестовых программ. В этом разделе описываются распространенные подходы к тестированию микропроцессоров, делается обзор работ, посвященных генерации тестовых программ, анализируются достоинства и недостатки существующих методов. Третий раздел посвящен описанию предлагаемого подхода. В нем вводятся основные понятия подхода, 25
описывается используемая для генерации модель микропроцессора, рассматривается метод генерации и устройство генератора; изложение иллюстрируется примерами, основанными на системе команд MIPS64 [5]. В четвертом разделе описывается опыт практического применения подхода для тестирования микропроцессоров. Пятый раздел завершает статью и очерчивает направления дальнейших исследований.
2. Методы построения тестовых программ Методами построения тестовых программ для функционального тестирования микропроцессоров занимаются, пожалуй, с момента появления микропроцессоров. Тем не менее, разработка новых подходов до сих пор привлекает внимание исследователей; более того, интерес к этой проблематике со временем только возрастает. На это есть своя причина — постоянно увеличивается сложность микропроцессоров, развивается архитектура, совершенствуются алгоритмы и схемы их функционирования — старые методы тестирования оказываются непригодными или малопригодными для качественной проверки микропроцессоров нового поколения. Качество тестирования любой системы (не обязательно микропроцессора) напрямую зависит от используемого тестового набора, от того, насколько полно он охватывает ситуации, возможные в работе системы; какие внутренние взаимодействия между модулями вызывает. Пространство состояний современных микропроцессоров чрезвычайно велико, поэтому разработка высококачественного набора тестов требует значительных затрат. Очевидно, что сегодня полностью ручная разработка тестов практически неприемлема, поскольку на тестирование в этом случае уйдут годы, и не исключено, что к моменту его завершения микропроцессор уже потеряет свою актуальность на рынке.
2.1. Распространенные подходы В настоящее время в практике тестирования микропроцессоров распространены следующие подходы: ручная разработка тестовых программ; тестирование с помощью существующего ПО; случайная генерация тестовых программ; случайная генерация тестовых программ на основе тестовых шаблонов. Несмотря на сказанное ранее, ручная разработка тестовых программ достаточно часто используется для проверки так называемых крайних случаев (corner cases) в работе микропроцессора. Разработчики таких тестов должны знать детали реализации тестируемого микропроцессора, чтобы создать соответствующие ситуации и проверить правильность поведения 26
микропроцессора в них. Большая опасность при таком подходе кроется в том, что важный для тестирования случай будет упущен (разработчик тестов может не придать значения тестовому случаю или по невнимательности забыть реализовать нужный тест), поэтому соответствующая ситуация в работе микропроцессора не будет реализована при тестировании [6]. Другим популярным методом функционального тестирования микропроцессоров является тестирование с помощью существующего ПО. Такое тестирование широко распространено и всегда проводится для микропроцессоров общего назначения. Как минимум, микропроцессор проверяют на одной или нескольких известных операционных системах. Как отмечают исследователи, программы, полученные таким образом, не дают гарантий относительно качества тестирования — это объемные тесты, которые широко, но не достаточно глубоко охватывают функциональность микропроцессора. Самым распространенным методом автоматического построения тестовых программ является случайная генерация. Программы, построенные таким образом, позволяют быстро обнаруживать простые ошибки. Другое достоинство случайных тестов состоит в том, что они могут создать ситуацию, которую сложно представить, но которая в то же время является интересной для тестирования [6]. Как уже отмечалось во введении, случайную генерацию нельзя назвать систематичным подходом — вероятность возникновения крайнего случая, как правило, очень мала, поэтому соответствующие ситуации скорее всего останутся непроверенными при тестировании. В настоящее время для построения тестовых программ широко используется случайная генерация на основе тестовых шаблонов (шаблонов тестовых программ) (test templates, test program templates). Тестовым шаблоном называется абстрактная форма представления тестовой программы. Шаблоны фиксируют или некоторым образом описывают последовательность инструкций тестовой программы. Вместо конкретных значений входных операндов инструкций в тестовых шаблонах указывается набор ограничений, которым операнды должны удовлетворять перед выполнением соответствующей инструкции. Генератор, используя некоторый механизм разрешения ограничений, строит случайное значение, удовлетворяющее заданным ограничениям. Такой подход также называется случайной генерацией на основе ограничений (constraint-driven random generation). Использование тестовых шаблонов позволяет значительно сократить трудозатраты, поскольку автоматизируется рутинная работа по подбору значений операндов, требуемых для создания тестовых ситуаций. Однако тестовые шаблоны, как правило, разрабатываются вручную, поэтому, как и при ручной разработке, есть возможность упустить важную для тестирования ситуацию. Ниже рассмотрены работы по генерации тестовых программ, большинство из которых посвящено методам автоматического построения тестовых шаблонов. 27
2.2. Обзор работ Компания IBM использует автоматические генераторы тестовых программ в процессе тестирования своих микропроцессоров, начиная с середины 1980-х [7]. Потребность в общем подходе, пригодном для широкого класса микропроцессорных архитектур, привела компанию к подходу на основе моделей. В рамках этого подхода генератор разбивается на два основных компонента: независимое от целевого микропроцессора ядро (engine) и модель (model), описывающую тестируемый микропроцессор. В 1991 году компанией был разработан генератор тестовых программ Genesys, который с 1993 года является основным генератором тестовых программ в IBM [8,9]. Генератор широко использовался как внутри компании, так и за ее пределами. В настоящее время в IBM разработана усовершенствованная версия генератора — Genesys-Pro [7]. Генератор тестовых программ Genesys-Pro использует три типа описаний: ядро, включающее общие принципы организации микропроцессоров и генерации тестов для них; модель, содержащую описание особенностей целевой архитектуры и некоторые знания о способах ее тестирования, и шаблоны тестовых программ, описывающие сценарии, которые нужно реализовать в тестовых программах. Генератор сводит задачу построения тестовых программ к задаче поиска решения системы ограничений (CSP, Constraint Satisfaction Problem), для решения которой используется общий механизм разрешения ограничений [7]. К достоинствам Genesys-Pro можно отнести выразительный язык описания шаблонов тестовых программ, который позволяет разработчикам тестов гибко управлять процессом генерации, а также удобную среду моделирования микропроцессорных архитектур. Подход является достаточно масштабируемым и универсальным. Недостатком, на наш взгляд, является ручная разработка тестовых шаблонов — многие шаблоны можно генерировать автоматически на основе описания архитектуры микропроцессора. Хотя этот недостаток можно исправить с помощью надстроек над генератором (см., например, [10]). Интересный подход к тестированию микропроцессоров с конвейерной архитектурой на основе проверки моделей (model checking) предлагается Мишрой (Mishra) из Университета Флориды (University of Florida) и Дуттом (Dutt) из Центра встроенных компьютерных систем Калифорнийского университета города Ирвин (Center for Embedded Computer Systems, University of California, Irvine). Этими исследователями (иногда в соавторстве с другими) в 2002-2006 годах написано несколько работ, описывающих предлагаемый подход и результаты его апробации [11-17]. Идея подхода состоит в следующем. На основе текстового описания архитектуры микропроцессора разрабатывается формальное описание на языке описания архитектуры (ADL, Architecture Description Language) EXPRESSION [18]. ADL-описание
28
транслируется в модель на SMV (Symbolic Model Verifier)2 [19]. Разработчик тестов указывает набор свойств (properties) в виде формул темпоральной логики, описывающих различные ситуации в работе конвейера, которые необходимо протестировать. Эти свойства отражают пути передачи управления и пути передачи данных между модулями конвейера. Авторы отмечают, что набор свойств такого вида может быть автоматически сгенерирован из ADL-описания. Модель микропроцессора и отрицания свойств подаются на вход SMV, который для каждого из указанных отрицаний пытается построить контрпример — вариант работы конвейера, в котором нарушается отрицание свойства — тем самым достигая описанную в исходном свойстве ситуацию. Построенные по контрпримерам тестовые программы выполняются на симуляторе, который, как и SMV-модель, может быть автоматически получен из ADL-описания. Результаты выполнения программ на симуляторе используются для проверки правильности работы микропроцессора. Для уменьшения времени генерации тестовых программ и затрат памяти, авторы предлагают использовать ограниченную проверку модели (BMC, Bounded Model Checking) [13] и декомпозицию свойств [14]. Предложенный подход был апробирован на микропроцессоре DLX3. Результаты апробации приведены в техническом отчете [12]. Достоинством подхода является его целенаправленность — один тест для покрытия одного свойства. Слабым местом подхода является необходимость описания всех интересных ситуаций в виде формул темпоральной логики. Конечно, как отмечают авторы, некоторый класс ситуаций может быть получен автоматически из ADL-описания, но это сравнительно небольшое множество крайних случаев. Следует также отметить, что, как и все подходы на основе проверки моделей, он подвержен комбинаторному взрыву состояний (state explosion problem). Для ограничения числа состояний в примере из технического отчета [12] авторы использовали модель памяти инструкций, состоящую всего из трех слов, а также всего три регистра общего назначения. Подход к генерации тестовых программ на основе конечно-автоматной модели конвейера, использующий техники обхода графа состояний, предлагается Уром (Ur) и Ядином (Yadin) из Исследовательского центра IBM 2
Язык и одноименный инструмент SMV разработаны в Университете Карнеги-Меллона (Carnegie Mellon University) МакМилланом (McMillan). Инструмент предназначен для проверки того, что система переходов с конечным числом состояний удовлетворяет спецификации, заданной в темпоральной логике. 3 DLX — микропроцессор, спроектированный Хеннесси (Hennessy) и Паттерсоном (Patterson) — главными разработчиками архитектур MIPS и Berkeley RISC соответственно. Микропроцессор имеет систему команд близкую к MIPS и предназначен преимущественно для образовательных и исследовательских целей. 29
в городе Хайфе (IBM Haifa Research Lab) [10]. Суть подхода в следующем. Вручную строится конечно-автоматная модель конвейера микропроцессора на языке SMV, в терминах которой определяются критерии тестового покрытия и генерируются абстрактные тесты. Абстрактным тестом называется тестовый шаблон, который описывает путь в графе состояний конечного автомата, начинающийся и заканчивающийся в состоянии, соответствующем пустому конвейеру. Абстрактные тесты генерируются с помощью специального инструмента CFSM [20], строящего множество маршрутов, покрывающих все дуги в графе состояний конечного автомата. Абстрактные тесты транслируются в описания тестовых шаблонов генератора Genesys, который по ним строит тестовые программы. Подход был успешно апробирован на микропроцессорах семейства PowerPC4. Достоинством подхода является сравнительно небольшое число тестов, достигающих хорошего покрытия управляющей логики микропроцессора. Авторы отмечают два недостатка своего подхода. Во-первых, необходим опытный эксперт для создания конечно-автоматной модели, описывающей работу конвейера с потактовой точностью. Во-вторых, для возможности отображения абстрактных тестов в конкретные последовательности инструкций необходимо создавать достаточно сложное описание в Genesys. Другой подход к генерации тестовых программ для микропроцессоров с конвейерной архитектурой на основе конечных автоматов предлагается Кохно (Kohno) и Мацумото (Matsumoto) из компании Toshiba [21]. Исследователи воплотили свой подход в инструменте mVpGen. Единственной входной информацией для mVpGen является спецификация конвейера, подробно описывающая его функционирование на уровне различных классов инструкций. На основе такой спецификации инструмент автоматически генерирует тестовые случаи (test cases) и конечно-автоматную модель конвейера. Тестовые случаи представляют собой состояния конвейера микропроцессора, в которых выполняющиеся инструкции вступают в конфликты чтения/записи (data hazards) или конфликты использования ресурсов (structural hazards). Переходы в конечно-автоматной модели определяются для классов инструкций и ограничений на значения операндов инструкций. По конечно-автоматной модели вычисляется множество достижимых тестовых случаев. Для каждого достижимого тестового случая строится тестовый шаблон — путь из начального состояния конечного автомата в состояние, соответствующее тестовому случаю. Наконец, по тестовым шаблонам генерируются тестовые программы. Подход был успешно апробирован в компании Toshiba на микропроцессоре MeP5. На наш взгляд, 4
PowerPC — известная микропроцессорная RISC-архитектура, созданная в 1991 году альянсом компаний Apple-IBM-Motorola, известном как AIM. 5 MeP (Media embedded Processor) — 32-x разрядный конфигурируемый RISCпроцессор обработки медиа данных, разрабатываемый компанией Toshiba. Первое ядро MeP (MeP-c1) разработано в 2001 году. 30
этот подход похож на предыдущий — в обоих подходах используются схожие модели. Основное отличие состоит в разных целях и задачах тестирования — в первом подходе совершается обход графа состояний конечного автомата, моделирующего конвейер, во втором подходе полный обход не совершается, а строятся пути только в те состояния, в которых возникают конфликты между инструкциями. Принципиально другой подход к генерации тестовых программ, основанный на использовании генетических алгоритмов, предлагается Корно (Corno), Кумани (Cumani), Сонца Реорда (Sonza Reorda), Сквиллеро (Squillero) и другими из Туринского политехнического университета (Politecnico di Torino). Этими исследователями в 2000-2005 годах написано большое число статей, описывающих предлагаемый подход, разработанный инструмент GP и результаты его апробации [22-35]. Генерация программ осуществляется на основе библиотеки инструкций, которая описывает синтаксис языка ассемблера целевого микропроцессора. Программа представляется как ациклический граф, каждая вершина которого содержит ссылку на описание инструкции в библиотеке инструкций и, если это необходимо, значения операндов. Тестовые программы строятся путем мутации структуры графа и значений операндов инструкций внутри отдельных вершин. Оценочная функция, на основе которой производится генерация, в зависимости от целей тестирования может быть разной, например, учитывать покрытие ошибок на уровне логических вентилей (gate-level fault coverage, stuck-at fault coverage) [25], покрытие инструкций RTL-модели (RTL statement coverage) [34] или внутренние счетчики производительности микропроцессора (performance counters) [33]. Подход был апробирован на нескольких микропроцессорах, включая микропроцессоры с конвейерной архитектурой DLX/pII [31] и SPARC v8 [26,35]. Предлагаемый подход является достаточно гибким и универсальным, он позволяет достичь высокого уровня тестового покрытия для различных метрик качества тестирования, но ценой большого времени генерации тестовых программ. Как правило, для каждой конкретной метрики существуют более эффективные алгоритмы генерации тестовых программ.
2.3. Выводы из анализа текущего состояния Подводя итоги, заметим, что многие исследователи видят преимущества использования моделей и метрик качества тестирования на их основе для генерации тестовых программ. Основой вопрос, который встает, — это вопрос о выборе адекватных моделей и адекватных метрик. Также заметим, что большое число работ посвящено тестированию на основе конечно-автоматных моделей, описывающих конвейер микропроцессора с потактовой точностью (будем называть такие модели точными моделями конвейера). Такие методы, как правило, предназначены для повышения покрытия, достигаемого существующими тестами [36,37] и для генерации тестов, нацеленных на сравнительно небольшое число крайних случаев [11-17], но они не являются 31
альтернативой массивному тестированию6, обеспечиваемому тестами на основе существующего ПО и случайной генерацией. Отметим следующие проблемы использования точных моделей конвейера. Есть два способа ее получения: либо модель извлекается автоматически на основе статического анализа кода RTL-описания микропроцессора [2,6,36,37], либо строится вручную [8,21]. Автоматическое извлечение модели является сложной задачей и требует наличия в коде аннотаций разработчика [2] или привлечения эвристик [36,37]. Для сложных микропроцессоров автоматическое извлечение конечно-автоматной модели управляющей логики практически неосуществимо. При построении модели вручную возникает другая проблема — построенную модель сложно отлаживать [10]. Поскольку на основе такой модели предполагается генерация тестов, важно чтобы модель точно описывала тестируемый компонент, так как в противном случае цели тестирования не будут достигнуты. Существующие подходы к тестированию микропроцессоров с помощью тестовых программ можно представить в виде следующей таблицы. Тестирование Массивное Немассивное
Ручное тестирование Существующее ПО Ручная разработка
Вероятностное тестирование Случайная генерация Тестовые шаблоны
Тестирование на основе моделей На основе автоматов
Таблица 2. Классификация имеющихся подходов к построению тестов. Таблица показывает, что для массивного тестирования микропроцессоров отсутствуют методы генерации тестовых программ на основе моделей. Общение с разработчиками микропроцессоров показывает, что потребность в подобных методах достаточно высока. Они привносят систематичность в массивное тестирование и позволяют увеличить уровень достигнутого тестового покрытия. В качестве основы таких методов можно использовать комбинаторное тестирование и случайную генерацию на основе тестовых шаблонов — разработчик тестов определяет тестовое покрытие для отдельных инструкций в виде наборов тестовых ситуаций, а генератор строит различные тестовые шаблоны, комбинируя тестовые ситуации для инструкций, которые могут повлиять друг на друга при выполнении на конвейере (инструкций, которые в тексте программы расположены близко). Именно к такому классу методов относится предлагаемый подход.
6
Массивными тестами называются тесты, которые имеют большой размер и широко охватывают функциональность тестируемой системы. Как правило, массивные тесты генерируются автоматически. 32
3. Описание предлагаемого подхода Предлагаемый в работе подход к генерации тестовых программ нацелен на массивное функциональное тестирование микропроцессоров. В этом смысле подход является альтернативой тестированию на основе существующего ПО и тестированию с помощью случайных программ. С другой стороны, подход можно рассматривать как развитие методов генерации на основе тестовых шаблонов, поскольку он оперирует с шаблонами, но они не разрабатываются вручную, а строятся генератором автоматически. Для генерации используется модель микропроцессора, включающая в себя структурную модель микропроцессора и модель системы команд. Цель генерации задается с помощью критерия тестового покрытия, выделяющего набор тестовых ситуаций для каждой инструкции микропроцессора. В методе реализуется направленный перебор всевозможных сочетаний тестовых ситуаций для последовательностей инструкций ограниченной длины. Как и все комбинаторные методы, он подвержен “комбинаторному взрыву” — размер тестов резко возрастает при увеличении длины тестируемых последовательностей инструкций, числа тестовых ситуаций и других параметров генерации. Для сокращения размера тестов используются дополнительные эвристики, например, гипотезы об эквивалентности некоторых инструкций между собой. Эквивалентные инструкции одинаковы с точки зрения управляющей логики микропроцессора — они затрагивают одни и те же модули, а их планирование и выполнение на конвейере осуществляется идентичным образом. Например, инструкции сложения и вычитания чисел одного типа можно считать эквивалентными, поскольку они выполняются одинаково. Какие инструкции являются эквивалентными, а какие нет, определяется на основе экспертных оценок. Идея подхода основана на предположении, согласно которому поведение микропроцессора (на каждом такте) зависит от множества выполняемых инструкций, зависимостей между ними (в том числе порядка их следования в тексте программы), а также от ситуаций, в которых оказываются выполняемые инструкции. Здесь говорится о множестве выполняемых инструкций, а не об одиночных инструкциях, поскольку большинство современных микропроцессоров являются конвейерными, и процессы выполнения инструкций в них могут пересекаться по времени [38]. Для качественного тестирования микропроцессоров с конвейерной архитектурой нужно по-разному “загружать” конвейер, используя в тестовых программах различные последовательности инструкций. Генератору на вход подаются описания тестируемых инструкций и тестовых ситуаций для них, возможные типы зависимостей между инструкциями, а также параметры, управляющие генерацией, например, длина генерируемых последовательностей инструкций. В общих словах, построение тестовых 33
программ осуществляется следующим образом. Генерируются всевозможные последовательности инструкций указанной длины. Для каждой последовательности инструкций строятся всевозможные множества зависимостей между ними. Для каждого множества зависимостей комбинируются всевозможные тестовые ситуации. Несколько слов об используемой терминологии. Обычно под значением операнда инструкции понимается номер регистра, если значение параметра передается через регистр, либо само значение, если оно передается непосредственно. Мы, для удобства, под значением операнда понимаем как номер регистра (если значение передается через регистр), так и значение параметра. Таким образом, значением операнда, в общем случае, является пара, состоящая из номера регистра и значения параметра. Под программой понимается произвольная конечная последовательность инструкций микропроцессора на языке ассемблера.
3.1. Основные понятия предлагаемого подхода В данном разделе рассматриваются основные понятия предлагаемого подхода к генерации тестовых программ: понятия тестового шаблона, зависимости между инструкциями, тестовой ситуации и тестового воздействия. Прежде чем определять эти понятия, рассмотрим, как устроены тестовые программы. В рамках предлагаемого подхода тестовые программы имеют вид = start {i, xi(Di, Si)}i=1,n stop, где: start — инициализирующая программа: содержит вспомогательные инструкции, предназначенные для инициализации микропроцессора; i, xi(Di, Si) — тестовый случай (i = 1, …, n): i — программа подготовки тестового воздействия: последовательность инструкций, осуществляющая инициализацию операндов тестируемых инструкций и подготовку состояния микропроцессора перед выполнением тестового воздействия; o xi(Di, Si) — тестовое воздействие: тестируемая последовательность инструкций: между инструкциями определены зависимости Di, а значения операндов инструкций и состояние микропроцессора (содержимое регистров, кэш-памяти и других подсистем) удовлетворяют ограничениям Si; stop — завершающая программа: содержит вспомогательные инструкции, предназначенные для завершения работы микропроцессора; n — размер тестовой программы: параметр генерации, указывающий число тестовых воздействий в одной тестовой программе. o
34
Ключевым понятием подхода является понятие тестового воздействия. Тестовое воздействие характеризуется тестовым шаблоном (тестируемой последовательностью инструкций), зависимостями между инструкциями (как операнды разных инструкций связаны между собой) и тестовыми ситуациями (ограничениями на значения операндов инструкций и состояние микропроцессора). Целью генерации является перебор всевозможных тестовых шаблонов небольшого размера и построение для каждого из них всевозможных зависимостей и тестовых ситуаций.
3.1.1. Тестовый шаблон Большинство современных микропроцессоров имеют конвейерную архитектуру. В таких микропроцессорах выполнение инструкций разбито на стадии, и возможны ситуации, когда некоторая стадия одной инструкции выполняется параллельно с некоторой стадией другой инструкции [38] (см. Рис. 1). Для качественного тестирования конвейерных микропроцессоров недостаточно изолированно протестировать все инструкции, даже если при этом используются разнообразные значения операндов и разнообразные состояния памяти микропроцессора — помимо этого тесты должны создавать различные состояния конвейера. Инструкция A Инструкция B
A1
A2 B1
...
B2
Практика показывает, что большинство ошибок в микропроцессорах обнаруживается на достаточно коротких последовательностях инструкций (обычно для обнаружения ошибки требуется 2–4 инструкции, более длинные последовательности нужны реже), поэтому при генерации тестовых программ можно использовать тестовые шаблоны небольшого размера. Однако даже в этом случае общее число тестовых шаблонов может быть очень велико (число всевозможных троек, составленных из 200 инструкций, равно 2003 = 8 000 000). Для сокращения числа тестовых шаблонов используется следующий подход. Близкие с точки зрения управляющей логики инструкции (то есть инструкции, планирование и выполнение которых осуществляется похожим образом) объединяются в классы эквивалентности. Тестовые шаблоны, в которых на одинаковых позициях находятся эквивалентные инструкции, считаются эквивалентными. Тестовые программы должны содержать все классы эквивалентности тестовых шаблонов; для построения тестового воздействия используется произвольный тестовый шаблон из соответствующего класса эквивалентности.
3.1.2. Зависимости между инструкциями
An ...
sub ... // тестовый шаблон не определяет значений операндов
Bn
{A2, B1} текущий такт
время
Рис.1. Выполнение двух инструкций на конвейере. В предлагаемом подходе для создания разнообразных состояний конвейера предназначены так называемые тестовые шаблоны (шаблоны тестовых воздействий). Тестовым шаблоном называется тестируемая последовательность инструкций без указания конкретных значений операндов. Задача тестового шаблона зафиксировать порядок инструкций в тестовом воздействии. Для одного тестового шаблона обычно генерируются множество тестовых воздействий, которые отличаются друг от друга зависимостями между инструкциями и тестовыми ситуациями. Ниже приведен пример тестового шаблона, состоящий из двух инструкций: инструкции сложения ADD и инструкции вычитания SUB: add ... // тестовый шаблон фиксирует порядок инструкций 35
Использование различных тестовых шаблонов, как правило, не является достаточным условием качественного тестирования микропроцессора. Важным моментом является то, какие зависимости между инструкциями использовались в тестах. Микропроцессор может по-разному планировать выполнение инструкций или по-разному осуществлять блокировки в зависимости от того, как связаны между собой инструкции. В своем подходе мы выделяем два основных типа зависимостей между инструкциями: зависимости по регистрам и зависимости по адресам. Зависимости по регистрам выражаются в совпадении регистров, использующихся в качестве операндов разных инструкций тестового воздействия. Зависимости по адресам определены для инструкций работы с памятью, таких как инструкции загрузки и сохранения регистров (load/store instructions). Зависимости по адресам являются частным случаем так называемых зависимостей по содержимому7, которые определяются не на основе совпадения или несовпадения используемых регистров, а с помощью ограничений на значения пары входных параметров (зависимого и определяющего). 7
Зависимости по содержимому имеют смысл, когда значения операндов (или функций от значений операндов) адресуют некоторые разделяемые ресурсы, то есть имеют смысл адреса. 36
Мы используем две разновидности зависимостей по регистрам: зависимости типа определение-использование (define-use dependencies), когда выходной регистр одной инструкции является входным регистром следующей за ней инструкции (либо между ними находятся другие инструкции, которые не переопределяют значение этого регистра), и зависимости типа определениеопределение (define-define dependencies), когда выходной регистр одной инструкции также является выходным регистром следующей за ней инструкции (тоже могут быть промежуточные, не затрагивающие этот регистр, инструкции). Возможны и другие типы зависимостей, например, зависимости типа использование-использование (use-use dependencies), но такие зависимости обычно не влияют на логику работы микропроцессора. Для иллюстрации зависимостей по регистрам рассмотрим два простых примера. В первом примере показана зависимость типа определениеиспользование: add r1, r2, r3 // определение регистра r1 sub r4, r1, r5 // использование регистра r1
младшие 12 бит адреса, которые у виртуального и физического адреса в MIPS64-совместимых микропроцессорах совпадают [5]. Обозначим эту зависимость как L1RowEqual. Физический адрес в микропроцессорах семейства RM7000 состоит из 36 бит и имеет следующую структуру: биты с 0-ого по 2-ой включительно (byte bits) определяют позицию байта внутри двойного слова, биты с 3-его по 4-ый (dword bits) — позицию двойного слова в строке кэш-памяти, биты с 5-ого по 11-ый (index bits) — номер сроки и, наконец, биты с 12-ого по 35-ый (upper address bits) — тэг данных, используемый для проверки попадания данных в кэш-память [39]. Тэг данных Номер строки в кэше Номер двойного слова Номер байта 35 12 11 5 4 3 2 0
Инструкция ADD складывает содержимое регистров r2 и r3 и сохраняет результат в регистре r1. Следующая за ней инструкция SUB использует содержимое регистра r1 — она вычитает из содержимого регистра r1 содержимое регистра r5 и сохраняет результат в регистре r4. В следующем примере проиллюстрирована зависимость типа определениеопределение: add r1, r2, r3 // определение регистра r1 sub r1, r4, r5 // переопределение регистра r1
Таким образом, хранящиеся в памяти данные отображаются в одну строку кэша данных L1 тогда и только тогда, когда биты index bits их физических адресов совпадают. Зависимости между инструкциями описываются набором атрибутов; конкретный набор значений атрибутов фиксирует зависимость рассматриваемого типа. Помимо атрибутов описание зависимости включает в себя следующие компоненты: итератор — перебирает все допустимые комбинации значений атрибутов зависимости; фильтр — проверяет множество зависимостей данного типа, относящихся к одному зависимому операнду, на непротиворечивость; конструктор — конструирует (полностью или частично) значение зависимого операнда по значениям операндов, от которых он зависим через зависимости данного типа; предусловие — проверяет допустимость использования зависимости данного типа для пары операндов различных инструкций.
В этом примере инструкция SUB сохраняет свой результат в том же самом регистре r1, что и предшествующая ей инструкция ADD. Зависимости по адресам определяются структурой памяти микропроцессора. Мы выделяем зависимости по виртуальным адресам и зависимости по физическим адресам. Использование в тестах различных зависимостей по адресам позволяет улучшить качество тестирования подсистемы трансляции адресов (для зависимостей по виртуальным адресам) и подсистемы управления кэш-памятью (для зависимостей по физическим адресам). В качестве примеров зависимостей по адресам можно привести следующие: совпадение виртуальных адресов, используемых различными инструкциями; совпадение физических адресов при несовпадающих виртуальных адресах; попадание в одну и ту же запись буфера трансляции адресов; попадание в одну и ту же строку кэш-памяти. Проиллюстрируем зависимости по адресам на примере микропроцессоров семейства RM7000 [39]. Рассмотрим зависимость следующего вида — совпадение номера строки в кэше данных L1. Данная зависимость является скорее зависимостью по физическим адресам, хотя ее можно отнести и к зависимостям по виртуальным адресам, поскольку она затрагивает только 37
Рис. 2. Формат физического адреса в RM7000.
Рассмотрим, как устроены указанные выше компоненты на примере зависимости L1RowEqual. Зависимость описывается одним атрибутом, который принимает два значения: true или false. Итератор зависимости перебирает эти значения в некотором порядке. Фильтр проверяет, что в множестве зависимостей существует не более одной зависимости, у которой атрибут имеет значение true (в противном случае возникает противоречие или избыточность). Если в множестве зависимостей существует такая, у которой атрибут имеет значение true, конструктор присваивает битам index bits зависимого операнда значение соответствующих бит адреса, от которого он зависит; если у всех зависимостей атрибут имеет значение false, конструктор 38
генерирует случайное значение, отличное от всех значений index bits, используемых в адресах, от которых зависит конструируемый операнд. Предусловие зависимости проверяет, что виртуальные адреса, используемые в операндах зависимых инструкций, могут быть преобразованы в физические — в противном случае, например, когда используются ошибочные адреса, обращения к кэш-памяти (по крайней мере для одной инструкции) не производится, поэтому зависимость не имеет смысла. Типы зависимостей, которые целесообразно использовать для тестирования, определяются на основе анализа документации по тестируемому микропроцессору. Определение зависимостей по регистрам обычно сводится к разбиению множества регистров на типы: регистры общего назначения (GPR, General Purpose Registers), системные регистры (system registers), регистры модуля арифметики с плавающей точкой (FPR, Floating Point Registers) — зависимости возможны только между регистрами одного типа. При определении зависимостей по адресам следует обращать внимание на структуру буфера трансляции адресов и кэш-памяти (L1, L2 и других уровней, если они присутствуют).
3.1.3. Тестовые ситуации Как правило, инструкции микропроцессора имеют несколько ветвей функциональности, то есть в зависимости от значений входных операндов и состояния микропроцессора ведут себя по-разному. Для инструкций, вызывающих исключения, примерами ветвей функциональности являются ситуации, в которых возникает то или иное исключение. Обязательным условием качественного тестирования микропроцессора является покрытие всех ветвей функциональности всех реализуемых им инструкций. Далее вместо термина ветвь функциональности мы используем более общий термин тестовая ситуация. Под тестовой ситуацией для инструкции понимается ограничение на значения входных операндов и состояние микропроцессора перед началом выполнения инструкции. Если рассматривать не отдельную инструкцию, а их последовательность (тестовый шаблон), можно определить тестовую ситуацию для шаблона в целом как набор тестовых ситуаций для входящих в него инструкций. Тестовые ситуации выявляются на основе анализа описания архитектуры микропроцессора и его системы команд. Особое внимание при этом уделяется ветвям функциональности, в частности, условиям на исключения. Рассмотрим пример описания инструкции ADD из системы команд MIPS64 [5]:
if NotWordValue(GPR[rs])or NotWordValue(GPR[rt]) then UNPREDICTABLE endif temp GPR[rs]31||GPR[rs]31..0)+(GPR[rt]31||GPR[rt]31..0) if temp32 temp31 then SignalException(IntegerOverflow) else GPR[rd] sign_extend(temp31..0) endif Предикат NotWordValue(GPR[rs]) or NotWordValue(GPR[rt]) является отрицанием предусловия инструкции. В рассматриваемом примере оба входных регистра должны содержать 32-х битные слова (значения битов с 32ого по 63-ый включительно совпадают со значением 31-ого бита). Предикат temp32 temp31 определяет ветвь функциональности, соответствующую исключению IntegerOverflow; другая ветвь соответствует нормальному выполнению инструкции. Также, как и зависимости между инструкциями, тестовые ситуации описываются набором атрибутов, а конкретный набор значений атрибутов фиксирует тестовую ситуацию рассматриваемого типа. Помимо атрибутов описание тестовой ситуации включает в себя следующие компоненты: итератор — перебирает все допустимые комбинации значений атрибутов тестовой ситуации; фильтр — проверяет тестовую ситуацию на непротиворечивость тестовым ситуациям предшествующих инструкций с учетом существующих зависимостей между инструкциями8; конструктор — конструирует значения операндов инструкции с учетом зависимостей от предшествующих инструкций; компонент подготовки — строит программу подготовки тестовой ситуации для инструкции, то есть программу, которая инициализирует операнды инструкции и подготавливает нужным образом состояние микропроцессора. Рассмотрим пример. Семейство тестовых ситуаций для инструкции ADD (на некотором уровне абстракции) описывается одним атрибутом IntegerOverflow, который принимает два значения: true или false. Если значение атрибута равно true, это означает, что содержимое входных регистров инструкции должно быть таким, чтобы при их сложении возникало переполнение; в противном случае переполнения быть не должно. 8
39
На практике можно использовать более “слабые” условия фильтрации, например, фильтр может пропускать тестовые ситуации, которые протииворечат зависимостям, если при этом не нарушаются предусловия инструкций. 40
Фильтр тестовой ситуации проверяет, связаны ли входные регистры инструкции зависимостью типа определение-использование с выходными регистрами предшествующих инструкций: если да, и зависимыми являются оба регистра, проверяется, согласуется ли их содержимое со значением атрибута IntegerOverflow; если нет, такая ситуация отбрасывается; если же зависимым является только один регистр, ситуация отбрасывается, только если значение IntegerOverflow равно true, а значение зависимого регистра — 0. Если обозначить через x и y значения операндов, а через temp результат сложения x и y, расширенных знаковым битом, то есть (x31 || x31..0) + (y31 || y31..0), то условие на переполнение примет вид temp31 temp32. Конструктор тестовой ситуации генерирует случайные значения независимых операндов, которые удовлетворяют этому ограничению, если атрибут IntegerOverflow имеет значение true, и не удовлетворяют ему, если атрибут IntegerOverflow имеет значение false. Компонент подготовки тестовой ситуации для каждого независимого входного регистра добавляет в программу подготовки последовательность инструкций, осуществляющую загрузку в него сконструированного значения.
3.1.4. Тестовые воздействия Как было отмечено в начале раздела, основным понятием в предлагаемом подходе является понятие тестового воздействия. Тестовым воздействием называется последовательность инструкций с заданными значениями операндов, выполнение которой начинается в заданном состоянии микропроцессора. Будем называть тестовые воздействия эквивалентными, если у них совпадают тестовые шаблоны, зависимости между инструкциями и тестовые ситуации. Под обобщенными тестовыми воздействиями будем понимать классы эквивалентности тестовых воздействий. Целью генерации является реализация в тестовых программах всех обобщенных тестовых воздействий. Для достижения этой цели решаются две задачи: перебор обобщенных тестовых воздействий и подготовка (конструирование) тестовых воздействий. При переборе осуществляется последовательное построение всевозможных тестовых шаблонов, зависимостей между инструкциями и тестовых ситуаций. Подготовка тестового воздействия состоит в построении программы, которая инициализирует операнды инструкций тестового воздействия и подготавливает состояние микропроцессора таким образом, чтобы они соответствовали тестовой ситуации. По сути, на каждом шаге перебора формируется набор ограничений на тестовое воздействие, которые разрешаются на этапе подготовки тестового воздействия. Для перебора обобщенных тестовых воздействий используются итераторы зависимостей и итераторы тестовых ситуаций, для подготовки тестовых воздействий — конструкторы зависимостей, а также конструкторы и компоненты подготовки тестовых ситуаций. Более подробно вопросы, 41
относящиеся к генерации, рассмотрены в разделе “Метод генерации и устройство генератора”.
3.2. Модель микропроцессора Для генерации тестовых программ используется модель микропроцессора. Модель имеет два аспекта: статический и динамический. Первый из них состоит в том, что модель является статическим описанием архитектуры микропроцессора и его системы команд. Описание, будучи формальным, позволяет генератору автоматически анализировать модель и использовать результаты анализа при построении тестовых программ. Второй аспект заключается в том, что в процессе генерации (в динамике) модель отражает текущее состояние микропроцессора. Модельное состояние позволяет контролировать предусловия инструкций и корректно подготавливать тестовые ситуации. Заметим также, что модельное состояние можно использовать для построения так называемых самопроверяющих тестов, то есть тестов, в которых после каждого тестового воздействия располагается тестовый оракул — программа, осуществляющая проверку правильности выполнения тестового воздействия микропроцессором. Модель микропроцесcора включает в себя модель архитектуры (структурную модель), модель типов данных, модель системы команд и модель конвейера. Рассмотрим каждую из перечисленных составляющих.
3.2.1. Модель архитектуры Модель архитектуры описывает основные подсистемы микропроцессора: регистры, буфер трансляции адресов, кэш-память и другие. Основное назначение модели архитектуры заключается в описании структуры состояния микропроцессора — это фундамент, на основе которого осуществляется описание системы команд, зависимостей между инструкциями и тестовых ситуаций. Важно отметить, что уровень абстракции, на котором моделируется архитектура микропроцессора, может варьироваться в зависимости от целей тестирования. Например, при моделировании кэш-памяти можно не включать в модель хранящиеся в кэше данные, а моделировать только тэги данных — этого достаточно для того, чтобы проверять и подготавливать тестовые ситуации такие как попадание или промах в кэш-память. Важной составной частью модели архитектуры является модель регистров. Модель регистров вводит на множестве регистров микропроцессора систему типов: регистры общего назначения, системные регистры, регистры модуля арифметики с плавающей точкой и другие. Особая роль этой модели связана с тем, что регистры являются основным способом передачи значений параметров инструкциям и получения результата их выполнения. При описании инструкций указываются типы их входных и выходных регистров; 42
эта информация используется генератором для построения зависимостей по регистрам.
инструкциями. В частности, зависимость по регистрам типа определениеиспользование возможна только в том случае, если типы выходного и входного регистров совместимы. Например, регистр, содержащий результат 32-x битной операции ADD, можно использовать в качестве операнда 64-х битной операции DSUB, но не наоборот.
3.2.3. Модель системы команд
Рис. 3. Основные моделируемые подсистемы микропроцессора.
3.2.2. Модель типов данных Разные инструкции микропроцессора определены над данными разных типов: одни — над 32-х битными словами, другие — над двойными словами; одни инструкции требуют, чтобы значение операнда являлось числом с плавающей точкой, другие — чтобы оно было виртуальным адресом и так далее. Модель типов данных описывает множество типов данных микропроцессора и отражает совместимость различных типов данных между собой. Как правило, микропроцессоры имеют достаточно простую систему типов: байт, полуслово, слово, двойное слово, числа с плавающей точкой одинарной и двойной точности, виртуальный адрес и некоторые другие.
Рис.4. Диаграмма основных типов данных микропроцессора. При описании инструкций указываются типы данных их операндов. Эта информация используется генератором при построении зависимостей между 43
Модель системы команд содержит основную информацию, с которой работает генератор тестовых программ: интерфейс, семантику и ассемблерный формат инструкций микропроцессора. Описание интерфейса используется генератором при построении зависимостей между инструкциями, на основе описания семантики генератор обновляет модельное состояние микропроцессора; ассемблерный формат используется при отображении внутреннего представления тестовых программ в файлы на языке ассемблера. Описание отдельной инструкции микропроцессора включает в себя следующие компоненты: интерфейс инструкции — описывает операнды инструкции: для каждого операнда указывается его имя, способ передачи значения (непосредственно или через регистр, в последнем случае также указывается тип регистра), тип данных и информация о том, является операнд входным или выходным;
предусловие инструкции — определяет ситуации, в которых определено поведение микропроцессора при выполнении данной инструкции. Автоматически генерируемой частью предусловия является проверка совместимости значений входных операндов соответствующим типам данным;
функция вычисления значений выходных операндов — вычисляет значения выходных операндов инструкции по значениям входных операндов, а также исключения, возникающие при выполнении инструкции;
функция обновления состояния микропроцессора — обновляет модельное состояние микропроцессора на основе значений входных операндов инструкции и текущего состояния микропроцессора;
отображение в ассемблерный код — определяет формат записи инструкции и ее операндов на языке ассемблера.
Отметим, что семантика инструкции описывается двумя функциями: функцией вычисления значений выходных операндов и функцией обновления состояния микропроцессора. Первая функция используется, когда при конструировании тестовой ситуации требуется определить значения 44
зависимых операндов соответствующей инструкции. Эти значения нельзя получить из модельного состояния микропроцессора, поскольку конструирование тестовой ситуации осуществляется до обновления состояния предшествующими инструкциями тестового воздействия (они будут выполнены только после выполнения программ подготовки тестовых ситуаций всех инструкций тестового воздействия). Вторая функция, как видно из названия, используется для обновления состояния микропроцессора. Модель системы команд микропроцессора разрабатывается на основе анализа документации. Современные руководства содержат детальные описания инструкций, которые могут быть легко формализованы. Для примера, рассмотрим фрагмент описания инструкции ADD из системы команд MIPS64 [5]. Format: ADD rd, rs, rt Description: rd rs + rt Operation: if NotWordValue(GPR[rs])or NotWordValue(GPR[rt]) then UNPREDICTABLE endif temp (GPR[rs]31||GPR[rs]31..0)+(GPR[rt]31||GPR[rt]31..0) if temp32 temp31 then SignalException(IntegerOverflow) else GPR[rd] sign_extend(temp31..0) endif Из этого описания видно, что у инструкции три операнда: rd, rs и rt; значения всех операндов передаются через регистры общего назначения (GPRs, General Purpose Registers); тип данных операндов — слово; операнд rd является выходным, операнды rs и rt — входными. Описание также определяет предусловие инструкции, ее семантику и отображение в ассемблерный код.
3.2.4. Модель конвейера Модель конвейера является необязательной частью модели микропроцессора и предназначена для более детального моделирования процесса выполнения инструкций. Использование модели конвейера позволяет описывать и создавать тестовые ситуации, связанные с параллельным выполнением инструкций на конвейере, например, ситуации, в которых между инструкциями возникают конфликты использования ресурсов микропроцессора. Заметим, что модель не обязательно должна описывать конвейер микропроцессора с потактовой точностью — уровень детализации описания зависит от целей тестирования. 45
Мы не будем подробно описывать модель конвейера, поскольку она используется только в особых случаях, и ее описание может затруднить общее понимание подхода. В общих словах, модель конвейера представляет собой автомат, который меняет свое состояние в зависимости от поданной на выполнение инструкции. Состояние такого автомата может использоваться для исключения конфликтов использования ресурсов, ослеживания слотов задержки (delay slots), а также для других целей.
3.3. Метод генерации и устройство генератора В данном разделе описывается метод генерации и устройство генератора тестовых программ, разработанного в соответствии с описанным подходом. На вход генератору подается модель микропроцессора, описания зависимостей между инструкциями и описания тестовых ситуаций. Из числа инструкций, описанных в модели системы команд, указываются так называемые тестируемые инструкции, используемые для составления тестовых воздействий. Также на вход генератору подаются параметры, управляющие построением тестовых программ.
3.3.1. Метод генерации В общих словах, генерация тестовых программ осуществляется следующим образом. В цикле перебираются тестовые шаблоны. Для каждого шаблона перебираются и конструируются множества зависимостей по регистрам. Для каждого множества зависимостей по регистрам комбинируются тестовые ситуации для инструкций тестового воздействия. Для каждого набора тестовых ситуаций перебираются множества зависимостей по адресам. Последовательно для каждой инструкции тестового воздействия сначала конструируются зависимости по адресам от предыдущих инструкций (если они есть), затем — тестовая ситуация, после чего для тестовой ситуации строится программа подготовки. Из программ подготовки тестовых ситуаций строится программа подготовки тестового воздействия, которая в сгенерированном коде помещается непосредственно перед тестовым воздействием. Процесс генерации продолжается до тех пор, пока не будут перебраны все обобщенные тестовые воздействия. Генерация осуществляется согласно следующей схеме:
46
получить очередной тестовый шаблон o
получить очередное множество зависимостей по регистрам
o
сконструировать зависимости по регистрам
o
получить очередную тестовую ситуацию для шаблона
получить очередное множество зависимостей по адресам
для каждой инструкции тестового воздействия:
проверить предусловие: o
если предусловие нарушено, перейти к очередной тестовой ситуации для шаблона
проверить наличие зависимостей по адресам: o
если зависимости существуют, сконструировать зависимости
сконструировать тестовую ситуацию для инструкции
проверить тестовую ситуацию для инструкции на противоречивость: o
определенной подсистемы микропроцессора. Разработчик упорядочивает фрагменты таким образом, чтобы каждый следующий фрагмент не влияет на подсистемы, подготовленные предыдущими фрагментами. Чтобы такое упорядочивание было возможно, в графе, отражающем влияние подготовки одной подсистемы на состояние других подсистем не должно быть циклов; в противном случае следует объединять компоненты сильной связности графа в более крупные подсистемы и строить программы подготовки для таких объединенных подсистем.
если тестовая ситуация противоречива, перейти к следующей тестовой ситуации для шаблона
получить программу подготовки тестовой ситуации для инструкции
построить программу подготовки тестового воздействия
выполнить программу подготовки тестового воздействия на модели
выполнить тестовое воздействие на модели
Рассмотрим подробнее, как строится программа подготовки тестового воздействия. На первый взгляд, задача является тривиальной — достаточно последовательно расположить программы подготовки тестовых ситуаций для инструкций тестового воздействия: сначала — программу подготовки тестовой ситуации для первой инструкции, затем для второй и так далее. В большинстве случаев такой способ работает, но не всегда. Бывают ситуации, когда программа подготовки тестовой ситуации влияет на предшествующие инструкции. Мы столкнулись с этой проблемой при тестировании инструкций загрузки и сохранения регистров. Для таких инструкций программа подготовки состоит из двух частей: подготовка буфера трансляции адресов и подготовка кэш-памяти. Программа подготовки буфера трансляции адресов изменяет состояние кэш-памяти, поэтому при построении программы подготовки тестового воздействия целесообразно сначала осуществлять подготовку буфера трансляции адресов для всех инструкций тестового воздействия, а затем — подготовку кэш-памяти (подготовка кэш-памяти не затрагивают буфера трансляции адресов, поскольку для этого используются адреса из неотображаемых сегментов виртуальной памяти). В общем случае, используемый в генераторе метод построения программ подготовки тестовых воздействий следующий. Разработчик тестов разбивает программы подготовки тестовых ситуаций для тестируемых инструкций на несколько фрагментов. Каждый фрагмент отвечает за инициализацию 47
Рис.5. Построение программы подготовки тестового воздействия. Отметим еще один важный момент. Программа подготовки тестовой ситуации для инструкции тестового воздействия зависит от состояния микропроцессора. Для того чтобы построить ее корректно, необходимо выполнить функции обновления состояния микропроцессора для всех предшествующих инструкций. Среди них есть инструкции, относящиеся к программам подготовки тестовых ситуаций для последующих инструкций тестового воздействия. В свою очередь, для корректного построения этих программ необходимо выполнить рассматриваемую инструкцию, но это возможно лишь после полного построения и выполнения программы подготовки тестового воздействия. Для того чтобы выйти из этого замкнутого круга, используются зависимости. Очевидно, что программу подготовки тестовой ситуации для первой инструкции можно построить корректно. Если инструкция не зависит по адресам от предшествующих (использует другую запись буфера трансляции адресов, другую строку кэш-памяти), при построении программы подготовки тестовой ситуации для нее можно использовать состояние микропроцессора до выполнения тестового воздействия. Если же зависимость по адресам существует, предполагается, что зависимую часть ситуации можно 48
подготовить, используя состояние микропроцессора до выполнения тестового воздействия и информацию о зависимости.
3.3.2. Архитектура генератора В генераторе можно выделить следующие основные компоненты: итератор тестовых шаблонов, предназначенный для перебора шаблонов тестовых воздействий; итератор зависимостей, отвечающий за перебор зависимостей между инструкциями, и итератор тестовых ситуаций, задача которого состоит в комбинировании тестовых ситуаций для инструкций тестового воздействия. Итератор тестовых зависимостей в свою очередь состоит из итератора зависимостей по регистрам и итератора зависимостей по адресам. Помимо основных компонентов, которые формируют ядро генератора, генератор содержит несколько вспомогательных библиотек.
Среди библиотек генератора можно отдельно выделить библиотеки подсистем, библиотеки типов данных, библиотеки итераторов и библиотеки генерации. Библиотеки подсистем предназначены для упрощения моделирования основных подсистем микропроцессора, таких как регистры, буфер трансляции адресов, кэш-память. К библиотекам подсистем также относятся библиотеки арифметики, которые реализуют основные арифметические операции, включая операции над числами с плавающей точкой. Библиотеки типов данных моделируют основные типы данных, используемые в микропроцессорах. Библиотеки итераторов содержат реализацию итераторов, часто используемых при разработке тестов. Библиотеки генерации отвечают за конструирование значений разных типов данных на основе ограничений. Заметим, что описания инструкций, которые подаются на вход генератору, могут объединяться в группы. Группы могут иметь иерархическую структуру. Группировка инструкций, как правило, производится на основе подсистем микропроцессора, задействованных для выполнения инструкций. Разбиение инструкций на группы дает генератору дополнительную информацию, которую он может использовать при построении тестовых программ.
3.3.3. Параметры управления генерацией Как было сказано ранее, на вход генератору помимо модели микропроцессора, описаний зависимостей и описаний тестовых ситуаций подаются параметры, управляющие построением тестовых программ, к числу которых относятся: классы эквивалентности инструкций — предназначены для сокращения числа тестовых воздействий. Предполагается, что эквивалентные инструкции одинаковы с точки зрения управляющей логики микропроцессора, и выбор той или иной инструкции из одного класса эквивалентности не влияет качество тестирования. Отметим, что эквивалентные инструкции должны иметь одинаковые наборы тестовых ситуаций;
Рис.6. Структура входных данных генератора и его архитектура. 49
50
размер тестового шаблона — указывает число инструкций в одном тестовом воздействии. На практике используются тестовые воздействия небольшой длины (из 2–4 инструкций, более длинные тестовые воздействия используются реже);
число тестовых воздействий в одной программе — управляет разбиением тестовых воздействий по тестовым программам. Как только в процессе генерации накапливается требуемое число тестовых воздействий, они записываются в файл.
шаблон имени файла — задает способ именования файлов с тестовыми программами.
3.3.4. Примеры
setException(new UnpredictableBehavior()); return;
В данном разделе в качестве иллюстрации ко всему вышесказанному приведены четыре примера. Первые три из них являются фрагментами описаний, которые подаются на вход генератору; четвертый — это часть сгенерированной тестовой программы. Примеры никак не связаны между собой — их можно рассматривать независимо друг от друга. Сразу следует сказать, что в существующей версии генератора разработка входных данных (модели микропроцессора, зависимостей между инструкциями и тестовых ситуаций) осуществляется на языке программирования Java. В дальнейшем мы планируем использовать специализированные языки описания микропроцессоров и тестов — в этом случае классы, подобные тем, которые мы используем сейчас, будут использоваться для внутреннего представления данных генератора. Ниже приведен фрагмент описания инструкции сложения ADD. // класс, описывающий инструкцию ADD public class ADDInstruction extends Instruction { public ADDInstruction() { super("add"); // операнды инструкции ADD addOperand(new Operand(this, OperandType.GPR_REGISTER, addOperand(new Operand(this, OperandType.GPR_REGISTER, addOperand(new Operand(this, OperandType.GPR_REGISTER, }
} int result = add(rsValue, rtValue); if(arithmeticOverflow32(result)) { setException(new IntegerOverflow()); return; } rdOperand.setIntegerValue(result); } // функция обновления состояния микропроцессора public void execute(Processor processor) { super.execute(processor); Operand rdOperand = getOperand("rd"); Operand rsOperand = getOperand("rs"); Operand rtOperand = getOperand("rt");
"rd", Operand.OUTPUT, ContentType.WORD)); "rs", Operand.INPUT, ContentType.WORD)); "rt", Operand.INPUT, ContentType.WORD));
Register rsRegister = rsOperand.getRegister(); Register rtRegister = rtOperand.getRegister(); Register rdRegister = rdOperand.getRegister(); int result = add(rsRegister.getIntegerValue(), rtRegister.getIntegerValue()); if(arithmeticOverflow32(result)) { setException(new IntegerOverflow()); } else { rdRegister.setIntegerValue(result); }
// функция вычисления значений выходных операндов public void calculate(Processor processor) { Operand rdOperand = getOperand("rd"); Operand rsOperand = getOperand("rs"); Operand rtOperand = getOperand("rt");
} // отображение инструкции в ассемблерный код public String toString() { Operand rd = getOperand("rd"); Operand rs = getOperand("rs"); Operand rt = getOperand("rt");
int rsValue = rsOperand.getIntegerValue(); int rtValue = rtOperand.getIntegerValue(); if(!WordTypeisWord(rsValue)||!WordTypeisWord(rtValue)) {
return "add " + rd.getRegister() + ", " + 51
52
rs.getRegister() + ", " + rt.getRegister(); } ... } Следующий пример является описанием зависимости по физическому адресу. //класс,описывающий зависимость по физическому адресу public class PADependency extends Dependency { // итераторы зависимости protected BooleanIterator l1TagEqual = new BooleanIterator(); protected BooleanIterator l1RowEqual = new BooleanIterator();
Приведенный ниже код описывает тестовую ситуации для инструкции деления DIV. // класс, описывающий тестовую ситуацию для инструкции DIV public class DIVSituation extends Situation { // итератор тестовой ситуации protected BooleanIterator divisinByZero = new BooleanIterator(); // конструктор тестовой ситуации public void construct(Processor GeneratorContext context) { Operand rsOperand = getOperand("rs"); Operand rtOperand = getOperand("rt");
// предусловие зависимости public boolean precondition() { MemorySituation situationDeps = (MemorySituation)getDepsSituation(); MemorySituation situationFrom = (MemorySituation)getFromSituation();
processor,
if(!context.isDefinedRegister(rsOperand.getRegister())) {rsOperand.setIntegerValue(Utils.random.nextInt());}
if(!context.isDefinedRegister(rtOperand.getRegister())) { if(divisionByZero.booleanValue()) { rtOperand.setIntegerValue(0); } else { int rtValue;
return situationDeps.canTranslateAddress() && situationFrom.canTranslateAddress(); } // конструктор зависимости public void construct(Situation ts, Dependencies deps, GeneratorContext context) { ... }
do { rtValue = Utils.random.nextInt(); } while(rtValue == 0)
// фильтр зависимости public boolean consistent(Operand operand) { ... }
rtOperand.setIntegerValue(rtValue); } } }
// описание зависимости public String toString() { return "l1TagEqual=" + l1TagEqual.booleanValue() + ", l1RowEqual=" + l1RowEqual.booleanValue(); } ...
// фильтр тестовой ситуации public boolean isConsistent() { return true; }
} 53
54
// функция подготовки тестовой ситуации public Program prepare(Processor GeneratorContext context) { Program program = new Program();
processor,
Operand rsOperand = getOperand("rs"); Operand rtOperand = getOperand("rt");
if(!context.isDefinedRegister(rsOperand.getRegister())) { program.append(new LoadImmediate32Program(processor, rsOperand.getRegister(), rsOperand.getIntegerValue()));
Последний пример — это часть сгенерированной тестовой программы, состоящая из одного тестового воздействия, предваренного программой подготовки. Несколько слов об используемых в примере инструкциях. Инструкция LUI сдвигает 16 битное значение (второй операнд) на 16 бит слева, расширяет полученное 32-х битное значение знаковым битом и записывает результат в регистр, указанный в качестве первого операнда. Инструкция ORI осуществляет побитовое ИЛИ значения регистра, указанного в качестве второго операнда, с 16 битным значением (третий операнд), результат записывается в регистр, указанный в качестве первого операнда; SUB, ADD и DIV — это инструкции вычитания, сложения и деления соответственно. ... // Подготовка sub[0]: IntegerOverflow=true // s5[rs]=0xffffffffc1c998db, v0[rt]=0x7def4297 lui s5, 0xc1c9 ori s5, s5, 0x98db lui v0, 0x7def ori v0, v0, 0x4297
context.useRegister(rsOperand.getRegister()); }
// Подготовка add[1]: IntegerOverflow=false // a0[rs]=0x1d922e27, a1[rt]=0x32bd66d5 lui a0, 0x1d92 ori a0, a0, 0x2e27 lui s3, 0x32bd ori s3, s3, 0x66d5
if(!context.isDefinedRegister(rtOperand.getRegister())) { program.append( new LoadImmediate32Program(processor, rtOperand.getRegister(), rtOperand.getIntegerValue()));
// Подготовка div[2]: DivisionByZero=true // a2[rs]=0x48f, a1[rt]=0x0 lui a2, 0x0 ori a2, a2, 0x48f lui a1, 0x0
context.useRegister(rtOperand.getRegister()); } return program; }
// Зависимости: div[2].rt[1]-sub[0].rd[0]
// описание тестовой ситуации public String toString() { return "DivisionByZero="+divisionByZero.booleanValue(); }
// Тестовое воздействие: 2008 sub a1, s5, v0 // IntegerOverflow=true add t7, a0, s3 // IntegerOverflow=false div a2, a1 // DivisionByZero=true ...
... }
55
56
4. Опыт практического применения подхода Описанный подход был использован в двух проектах по тестированию разных MIPS64-совместимых микропроцессоров. В первом проекте тестировалась только подсистема управления памятью (MMU, Memory Management Unit). Для этого в тестовых воздействиях использовались инструкции загрузки и сохранения регистров. Во втором проекте проводилось системное тестирование — для построения тестовых воздействий использовались все инструкции микропроцессора. Мы использовали традиционную процедуру проверки правильности поведения RTL-модели микропроцессора — одни и те же программы выполнялись на тестируемой модели и эталонном симуляторе; результаты выполнения программ сравнивались на соответствие. Результатом выполнения программы является трасса — последовательность, содержащая основные события, возникшие при выполнении инструкций программы (результаты выполнения, исключения и другие).
4.1. Тестирование подсистемы управления памятью Память современных компьютеров представляет достаточно сложную иерархию запоминающих устройств различных объемов, стоимости и времени доступа [38]. Помимо регистров и оперативной памяти в микропроцессорах имеется, по крайней мере, одноуровневая, а чаще двухуровневая кэш-память. Для ускорения преобразования виртуальных адресов в физические в микропроцессорах используются буферы трансляции адресов. Логически связанный набор модулей микропроцессора, отвечающих за организацию работы с памятью, называется подсистемой управления памятью. Мы применили описанный подход для тестирования подсистемы управления памятью MIPS64-совместимого микропроцессора. В качестве тестовых воздействий использовались всевозможные пары, составленные из четырех инструкций: LB (загрузка байта), LD (загрузка двойного слова), SB (сохранение байта) и SD (сохранение двойного слова). Тестовые ситуации для инструкций описывались следующими атрибутами: isMapped — отображаемый/неотображаемый сегмент виртуальной памяти; isCached — кэшируемый/некэшируемый сегмент виртуальной памяти; jtlbHit — попадание/промах в TLB; DVG — управляющие биты секции TLB; mtlbHit — попадание/промах в DTLB9; cachePolicy — политика кэширования; l1Hit — попадание/промах в кэш-память L1; 9
DTLB (Data TLB) — это небольшой буфер, который кэширует обращения к TLB при трансляции адресов данных. 57
l2Hit — попадание/промах в кэш-память L2.
При генерации тестовых программ использовались следующие зависимости по адресам: vaEqual — совпадение/несовпадение виртуальных адресов; tlbEqual — совпадение/несовпадение записи TLB; pageEqual — совпадение/несовпадение секции в записи TLB10; paEqual — совпадение/несовпадение физических адресов; l1RowEqual — совпадение/несовпадение номера строки в кэш-памяти L1; l2RowEqual — совпадение/несовпадение номера строки в кэш-памяти L2; mtlbReplace — совпадение/несовпадение записи TLB, используемой второй инструкцией, с записью, вытесненной из DTLB первой инструкцией; l1Replace — совпадение/несовпадение тэга кэш-памяти L1, вычисленного по физическому адресу, используемого во второй инструкции, с тэгом данных, который был вытеснен из кэш-памяти первой инструкцией; l2Replace — совпадение/несовпадение тэга кэш-памяти L2, вычисленного по физическому адресу, используемого во второй инструкции, с тэгом данных, который был вытеснен из кэш-памяти первой инструкцией. В результате тестирования было найдено несколько критических ошибок в подсистеме управления кэш-памятью, которые не были обнаружены с помощью тестовых программ, сгенерированных случайно.
4.2. Системное тестирование микропроцессора Более масштабным применением описанного подхода является системное тестирование другого MIPS64-совместимого микропроцессора. В качестве тестовых воздействий использовались тройки инструкций, составленные из всех 221 инструкции микропроцессора11. 10
В MIPS64-совместимых микропроцессорах запись TLB состоит из двух секций: одна для страницы (VPN, Virtual Page Number) с четным номером, вторая для страницы с нечетным номером. Запись адресуется номером страницы без младшего бита (VPN2), младший бит конкретизирует используемую секцию. 11 Инструкции, которые отличаются форматом операндов, например, ADD.S (сложение чисел с плавающей точкой одинарной точности) и ADD.D (сложение чисел с плавающей точкой двойной точности), при подсчете считались разными инструкциями. 58
Инструкции были условно разбиты на 13 групп: целочисленная арифметика (33 инструкции); логические операции (8 инструкций); перемещение (8 инструкций); сдвиги (15 инструкций); переходы (20 инструкций); пустые операции (2 инструкции); загрузка/сохранение (26 инструкций); исключения (14 инструкций); сопроцессор CP0 (13 инструкций); арифметика с плавающей точкой (24 инструкции); перемещение в FPU (Floating Point Unit, модуль арифметики с плавающей точкой) (26 инструкций); преобразование типов (26 инструкций); ветвление в FPU (6 инструкций). Поскольку число инструкций достаточно велико, для сокращения общего размера тестовых программ мы использовали дополнительную эвристику — тестовые воздействия включали инструкции не более чем из двух различных групп. К моменту написания статьи было выполнено около 70% тестов. В результате было найдено 5 ошибок в RTL-модели микропроцессора и 5 ошибок в эталонном симуляторе. Ниже приводятся некоторые из найденных ошибок. Рассмотрим пример ошибки, для обнаружения которой важна структура тестового шаблона12. Это ошибка была обнаружена в RTL-модели микропроцессора. div r1, r2 mthi r3 mfhi r4 Первая инструкция делит содержимое регистра r1 на содержимое регистра r2 и записывает полученный результат (частное и остаток) в регистры lo и hi. Следующая за делением инструкция сохраняет содержимое регистра r3 в регистре hi. Третья инструкция читает значение регистра hi и записывает его значение в регистр r4. Ошибка заключается в том, что последняя инструкция вместо значения, записанного второй инструкцией, считывает из регистра hi результат деления. Чтобы обнаружить следующую ошибку, важен как тестовый шаблон, так и тестовая ситуация. Эта ошибка, также как и предыдущая, была обнаружена в RTL-модели микропроцессора. 12
В данном примере из вида тестового шаблона вытекают некоторые зависимости по регистрам. 59
ctc1 r1, $25 movt r2, r3, $fcc0 cvt.s $f1, $f2 // Inexact=true Первая инструкция заносит данные в управляющий регистр FPU. Вторая инструкция делает пересылку в зависимости от битов условий (condition codes), содержащихся в FPU. Третья инструкция совершает операцию над числами с плавающей точкой, которая вызывает исключение потери точности Inexact. Ошибка возникает при разрешении одновременного выполнения двух инструкций (целочисленной инструкции и инструкции над числами с плавающей точкой), а также разрешении обработки исключения Inexact (установлен разряд Enables.Inexact в регистре FCSR) и проявляется как “зависание” микропроцессора. Последний пример иллюстрирует ошибку, для обнаружения которой важны тестовый шаблон, зависимости между инструкциями и тестовые ситуации. Это ошибка была обнаружена в симуляторе микропроцессора: add r1, r2, r3 // IntegerOverflow=true sub r4, r1, r5 Первая инструкция складывает содержимое регистров r2 и r3 и помещает результат в регистр r1. Содержимое регистров r2 и r3 должно быть таким, чтобы при их сложении возникало исключение переполнения IntegerOverflow. В этом случае, согласно описанию системы команд MIPS64, значение регистра r1 не должно измениться, поэтому следующая за сложением инструкция вычитания использует значение r1, которое было загружено в регистр программой подготовки. Ошибка заключается в том, что симулятор при возникновении исключения записывает в регистр r1 “мусор”, который не является значением 32-х битного слова. При использовании этого значения в инструкции вычитания симулятор останавливается из-за внутренней ошибки.
4.3. Отладка тестовых программ Опыт генерации тестовых программ показывает, что следует уделять большое внимание отладке компонентов тестов. Ошибки в описании тестов могут приводить к тому, что сгенерированные тестовые программы содержат не все тестовые воздействия или подготавливают их некорректным образом. Мы выделяем два уровня отладки тестов: проверка соответствия тестов модели микропроцессора и проверка соответствия модели микропроцессора эталонному симулятору. Для отладки первого типа мы использовали подход на основе пред- и постусловий основных функций, реализуемых компонентами теста: функций конструирования зависимостей и тестовых ситуаций, функций подготовки тестовых ситуаций и других. Это позволяет находить значительное число ошибок, особенно в конструкторах. Отладку второго типа осуществлять значительно сложнее. Грубые ошибки, такие как несовместимость типов или конфликты использования ресурсов, находятся с 60
помощью прогона тестовых программ в симуляторе. Чтобы обнаружить более сложные ошибки, необходим детальный анализ трассы выполнения тестовых программ.
5. Заключение В статье был рассмотрен подход к автоматической генерации тестовых программ для микропроцессоров. В отличие от распространенных на практике методов, таких как тестирование на основе существующего ПО и тестирование на основе случайных программ, предлагаемый подход является значительно более систематичным и технологичным. Кроме того, поскольку он основан на использовании моделей, он позволяет формализовать критерии качества тестирования. Методы тестирования, предлагаемые в исследовательских работах, ориентированы, прежде всего, на достижение сравнительно небольшого числа “крайних случаев”; для этого используются модели, описывающие микропроцессор с потактовой точностью. В отличие от таких узконаправленных методов наш подход использует более простые, понятные разработчикам модели и генерирует при этом массивные тесты, включающие все сочетания инструкций микропроцессора, интересные для тестирования. Это позволяет сократить затраты на разработку тестов, обеспечив при этом приемлемое качество тестирование, которое (что очень важно) может быть улучшено путем детализации модели и критериев тестового покрытия. Если говорить о направлениях дальнейшей работы, то в первую очередь следует сказать о доработке генератора с уровня прототипа до уровня полноценного инструмента. Есть много пунктов, по которым такую доработку можно осуществить. Например, в существующей версии генератора описания инструкций, зависимостей между инструкциями и тестовых ситуаций делаются на языке Java, хотя было бы удобнее использовать для этих целей специализированные языки. Другой интересной возможностью является повышение уровня автоматизации разработки тестов. К примеру, конструкторы тестовых ситуаций, которые сейчас пишутся вручную, можно попробовать реализовать с использованием системы разрешения ограничений. Это позволит описывать тестовые ситуации декларативно, в форме предикатов, а их построение будет осуществляться полностью автоматически. Литература [1] MIPS R4000PC/SC Errata, Processor Revision 2.2 and 3.0. MIPS Technologies Inc., May 1994. [2] R. Ho, C. Han Yang, M. Horowitz, D. L. Dill. Architecture Validation for Processors. ISCA’1995: International Symposium on Computer Architecture, June 1995. [3] http://www.ispras.ru. [4] http://www.unitesk.com.
61
[5] MIPS64™ Architecture For Programmers. Revision 2.0. MIPS Tecnologies Inc., June 2003. [6] R. Ho. Validation Tools for Complex Digital Designs. PhD Thesis. November, 1996. [7] A. Adir, E. Almog, L. Fournier, E. Marcus, M. Rimon, M. Vinov, A. Ziv. Genesys-Pro: Innovations in Test Program Generation for Functional Processor Verification. Design and Test, 2004. [8] http://www.haifa.ibm.com/projects/verification/GenesysPresent/index.htm. [9] L. Fournier, Y. Arbetman, M. Levinger. Functional Verification Methodology for Microprocessors Using the Genesys Test Program Generator: Application to the x86 Microprocessors Family. DATE’1999: Design Automation and Test in Europe, IEEE CS Press, 1999. [10] S. Ur and Y. Yadin. Micro Architecture Coverage Directed Generation of Test Programs. DAC’1999: Design Automation Conference, 1999. [11] P. Mishra, N. Dutt. Automatic Functional Test Program Generation for Pipelined Processors Using Model Checking. HLDVT’2002: Proceedings of the 7th IEEE International High-Level Design Validation and Test Workshop, 2002. [12] P. Mishra, N. Dutt. Architecture Description Language Driven Functional Test Program Generation for Microprocessors Using SMV. CECS Technical Report 02-26, September 2002. [13] H.M. Koo, P. Mishra. Test Generation Using SAT-based Bounded Model Checking for Validation of Pipelined Processors. ACM Great Lakes Symposium on VLSI, 2006. [14] H.M. Koo, P. Mishra. Functional Test Generation Using Property Decomposition for Validation of Pipelined Processors. DATE’2006: Design, Automation and Test in Europe, March 2006. [15] P. Mishra, N. Dutt. Graph-Based Functional Test Program Generation for Pipelined Processors. DATE’2004: Design, Automation and Test in Europe Conference and Exhibition, Volume 1, 2004. [16] P. Mishra, N. Dutt. Functional Coverage Driven Test Generation for Validation of Pipelined Processors. DATE’2005: Design, Automation and Test in Europe, Volume 2, 2005. [17] P. Mishra, N. Dutt, N. Krishnamurthy, M.S. Abadir. A Top-Down Methodology for Validation of Microprocessors. IEEE Design and Test, 2004. [18] P. Grun, A. Halambi, A. Khare, V. Ganesh, N. Dutt, A. Nicolau. EXPRESSION: An ADL for System Level Design Exploration. Technical Report 1998-29, University of California, Irvine, 1998. [19] www.cs.cmu.edu/~modelcheck/smv.html. [20] D. Geist, M. Farkas, A. Landver, Y. Lichtenstein, S. Ur, Y. Wolfsthal. Coverage Directed Test Generation Using Symbolic Techniques. FMCAD’1996: Formal Methods in Computer Aided Design, LNCS 1166, Palo Alto, CA, USA, November 1996. [21] K. Kohno, N. Matsumoto. A New Verification Methodology for Complex Pipeline Behavior. DAC’2001: Design Automation Conference, 2001. [22] F. Corno, G. Squillero. Exploiting Auto-Adaptive GP for Highly Effective Test Programs Generation. ICES’2003: The 5th International Conference on Evolvable Systems: From Biology to Hardware, Trondheim, Norway, March 2003. [23] F. Corno, M. Sonza Reorda, G. Squillero, M. Violante. A Genetic Algorithm-based System for Generating Test Programs for Microprocessor IP Cores. ICTAI’2000: The 12th IEEE International Conference on Tools with Artificial Intelligence, Vancouver, British Columbia, Canada, November 2000.
62
[24] F. Corno, M. Sonza Reorda, G. Squillero, M. Violante. On the Test of Microprocessor IP Cores. DATE’2001: IEEE Design, Automation and Test in Europe Conference, Munich, Germany, March 2001. [25] F. Corno, G. Cumani, M. Sonza Reorda, G. Squillero. Fully Automatic Test Program Generation for Microprocessor Cores. DATE’2003: Design, Automation and Test in Europe, Munich, Germany, March 2003. [26] E. Sanchez, M. Schillaci, M. Sonza Reorda, G. Squillero, L. Sterpone, M. Violante. New Evolutionary Techniques for Test-Program Generation for Complex Microprocessor Cores. GECCO’2005: Genetic and Evolutionary Computation Conference, Washington, DC, USA, June 2005. [27] F. Corno, G. Squillero. An Enhanced Framework for Microprocessor Test-Program Generation. EUROGP’2003: The 6th European Conference on Genetic Programming, Essex, UK, April 2003. [28] G. Squillero. MicroGP – An Evolutionary Assembly Program Generator. Genetic Programming and Evolvable Machines, Volume 6(3), 2005. [29] F. Corno, G. Cumani, M. Sonza Reorda, G. Squillero. Efficient Machine-Code TestProgram Induction. CEC’2002: Congress on Evolutionary Computation, Honolulu, Hawaii, USA, 2002. [30] F. Corno, G. Cumani, M. Sonza Reorda, G. Squillero. Evolutionary Test Program Induction for Microprocessor Design Verification. ATS’2002: IEEE Asian Test Symposium, Guam, USA, November 2002. [31] F. Corno, M. Sonza Reorda, G. Squillero. Automatic Test Program Generation for Pipelined Processors. SAC’2003: The 18th Annual ACM Symposium on Applied Computing, Melbourne, Florida, USA, March 2003. [32] F. Corno, E. Sanchez, M. Sonza Reorda, G. Squillero. Code Generation for Functional Validation of Pipelined Microprocessors. Journal of Electronic Testing: Theory and Applications, Volume 20(3), June 2004. [33] W. Lindsay, E. Sanchez, M. Sonza Reorda, G. Squillero. Automatic Test Programs Generation Driven by Internal Performance Counters. MTV’2004: The 5th International Workshop on Microprocessor Test and Verification, 2004. [34] F. Corno, G. Cumani, M. Sonza Reorda, G. Squillero. Automatic Test Program Generation from RT-Level Microprocessor Descriptions. ISQED’2002: The 3rd International Symposium on Quality Electronic Design, San Jose, California, USA, March 2002. [35] F. Corno, E. Sanchez, M. Sonza Reorda, G. Squillero. Automatic Test Program Generation – A Case Study. IEEE Design and Test, Special Issue on Functional Verification and Testbench Generation, Volume 21, Issue 2, March-April 2004. [36] D. Moundanos, J. Abraham, Y. Hoskote. A Unified Framework for Design Validation and Manufacturing Test. ITC’1996: International Test Conference, 1996. [37] D. Moundanos, J. Abraham, Y. Hoskote. Abstraction Techniques for Validation Coverage Analysis and Test Generation. IEEE Transactions on Computers, Volume 47, 1998. [38] D. Patterson, J. Hennesy. Computer Organization and Design: The Hardware/Software Interface. 3rd Edition. The Morgan Kaufmann Series in Computer Architecture and Design, 2005. [39] RM7000 Family User Manual. Issue 1, May 2001.
63
T2C: технология автоматизированной разработки тестов базовой функциональности программных интерфейсов В. В. Рубанов, А. В. Хорошилов, Е. А. Шатохин {vrub, khoroshilov, spectre}@ispras.ru Аннотация. В статье обсуждается задача автоматизации разработки тестов базовой функциональности программных интерфейсов (API). Рассматриваются существующие решения по разработке таких тестов и описываются разработанные в ИСП РАН технология и инструментальные средства T2C, нацеленные на создание тестов «среднего» уровня по качеству тестирования и объему соответствующих трудозатрат. Приводится статистика результатов использования T2C в рамках проекта по тестированию интерфейсов системных библиотек Linux, описываемых стандартом LSB.
1. Введение Хорошо известно, что проверить корректность работы любой серьезной программной системы во всех ситуациях, которые могут встретиться в ходе ее реальной эксплуатации, невозможно. Поэтому тестирование программного обеспечения неизбежно связано с поиском компромисса между тщательностью проверок и ограничениями на имеющиеся ресурсы. Поиск оптимального решения зависит от множества факторов, определяющих специфику конкретного проекта. В данной работе мы рассмотрим разработку тестов, предназначенных для тестирования на соответствие требованиям стандарта программных интерфейсов на языке программирования Си. Такая постановка задачи предопределяет значение целого ряда факторов. Необходимость сопровождения тестов вместе с развитием стандарта. Наличие стандарта подразумевает существование достаточно качественного описания требований к функциональности тестируемой системы. Хотя для стандартов, находящихся в стадии активного развития, такое предположение не всегда оказывается верным. Потребность в простоте анализа обнаруживаемых ошибок, так как во многих случаях ошибки в реализации будут анализировать не разработчики тестов, а представители сторонних организаций, проверяющих свои продукты на соответствие требованиям стандарта. 65
Эти особенности формируют ключевые требования к результирующему тестовому набору, и тем самым, к подходу по его разработке. Но из них не следует никаких ограничений на возможные решения конфликта между детальностью тестирования и имеющимися ресурсами. Наиболее распространенным решением при разработке тестов на соответствие является выбор тестирования базовой функциональности, под которым понимается тестирование, обеспечивающее проверку корректности функционирования системы на нескольких основных вариантах её использования, включающих в себя и некоторые ошибочные ситуации. Распространенность такого выбора объясняется тем, что тестирование базовой функциональности позволяет с минимальными затратами выявить большинство наиболее грубых отклонений от стандарта. Существуют и альтернативные решения. В случаях, когда в короткие сроки требуется обеспечить покрытие очень большого количества функций, выбор падает на менее тщательное тестирование, например, такое, которое нацелено на проверку только минимальной работоспособности каждой функции. Одна из технологий разработки тестов такого вида — технология Azov — представлена в настоящем сборнике. В ситуациях, когда обеспечение точного соответствия стандарту является крайне важным и подкрепляется наличием достаточного количества ресурсов, выбирается более тщательное тестирование, например, такое, которое нацелено на проверку всех выделенных классов тестовых ситуаций для каждого отдельного аспекта функциональности каждой интерфейсной функции. Примером инструментов, поддерживающих разработку столь детальных тестов, которые при этом еще и остаются удобными для дальнейшего сопровождения и развития, является набор инструментов CTESK [1]. Такие особенности CTESK как: наличие средств для формального описания требований к тестируемой системе; поддержка автоматической генерации последовательностей тестовых воздействий при помощи динамического построения модели поведения тестируемой системы; широкий набор возможностей для задания метрик качества тестирования в терминах модели требований с автоматизацией сбора информации о достигнутом покрытии; помогают организовать систематический перебор всех тестовых ситуаций и упростить анализ тестового покрытия. Набор инструментов T2C, о котором пойдет речь в настоящей статье, нацелен на разработку тестов базовой функциональности. Превосходя возможности технологии Azov и уступая инструментам CTESK по уровню тщательности тестирования получаемых тестов, инструменты T2C позволяют эффективно достигать середины в балансе между качеством тестирования и ресурсами, 66
необходимыми для построения соответствующих тестов. При этом инструменты T2C поддерживают основные рекомендации по работе с требованиями при разработке тестов на соответствие, такие как составление каталога элементарных требований стандарта, обеспечение прослеживаемости требований стандарта в тестах и измерение качества тестирования в терминах покрытия элементарных требований. Данная статья построена следующим образом. В первом разделе мы рассмотрим ряд подходов, решающих близкие задачи, а также обсудим их достоинства и недостатки применительно к стоящим перед нами целям. Далее мы представим основные особенности инструментов T2C и поддерживаемого ими процесса разработки. Результаты будут проиллюстрированы на опыте применения данного подхода при разработке тестов на соответствие требованиям стандарта LSB [2] для ряда библиотек из стека GTK+ и fontconfig. В заключении мы обсудим возможные направления дальнейшего развития подхода и его интеграции с другими инструментами семейства UniTESK.
2. Средства разработки тестов базовой функциональности программных интерфейсов 2.1. MANUAL Тестовые системы, обеспечивающие тщательное тестирование, как правило, требуют для своей работы целый ряд сервисов нижележащей операционной системы. Поэтому в случаях, когда объектом тестирования является сама операционная система, для запуска таких тестов требуется относительная стабильность объекта тестирования. Для смягчения этой проблемы, а также для минимизации непреднамеренного воздействия тестовой системы на целевую систему часто применяется распределенная архитектура построения тестов, при которой для выполнения большей части задач используется вспомогательная инструментальная машина, а на тестируемой системе работает только небольшой тестовый агент. Но даже в таком случае, для взаимодействия тестового агента с инструментальной машиной необходима работоспособность отдельных компонентов тестируемой системы. По этой причине, прежде чем приступать к тщательному тестированию программных интерфейсов целевой системы, необходимо удостовериться в работоспособности ее ключевых компонентов. В проекте по тестированию POSIX-совместимой операционной системы реального времени для встраиваемых устройств, в отделе технологий программирования ИСП РАН был разработан подход MANUAL, предназначенный для разработки тестов, проверяющих базовую функциональность операционной системы. Эти тесты проверяли 67
работоспособность ключевых компонентов операционной системы перед началом тщательного тестирования тестовой системой, разработанной при поддержке инструментов CTESK. Тесты MANUAL представляют собой код на языке программирования Си, использующий макросы и функции библиотеки поддержки MANUAL. Каждое тестовое испытание записывается в виде отдельной функции, начинающейся с макроса TC_START(“имя тестового испытания”) и заканчивающейся макросом TC_END(). Тело теста состоит из трех частей: подготовке окружения и данных; непосредственно тестового воздействия и проверки его результатов; освобождения ресурсов. Проверка корректности работы тестируемой системы проводится при помощи функции tc_assert(проверяющее_выражение, “текст, описывающий ошибку”). Если проверяющее выражение оказывается равным false, то система считает, что произошла ошибка в тестируемой системе и выводит сообщение, описывающее ее. Кроме того, система автоматически отлавливает в ходе выполнения теста исключительные ситуации, что приравнивается к обнаружению ошибки. Система MANUAL поддерживает иерархическую композицию тестовых испытаний в пакеты. Для запуска тестов предусмотрено два режима: автоматический и интерактивный. В автоматическом режиме система выполняет указанный набор тестов или пакетов и сохраняет журнал их выполнения. В интерактивном режиме пользователю предоставляется возможность навигации по дереву пакетов вплоть до индивидуального теста и возможность запустить выбранный тест или пакет на исполнение. Основным недостатком системы MANUAL является ее слабая масштабируемость. Проблемы с масштабируемостью связаны с тем, что каждый отдельный тест представляет собой функцию на языке Си, что при росте тестового набора требует либо многочисленного дублирования кода, либо существенного объема достаточно рутинной ручной работы по структуризации кода. Отсутствие параметризации отдельных тестов, которое вполне оправдано при реализации простейших проверок работоспособности базовой функциональности целевой системы, является существенным препятствием для применения подхода к разработке более детальных тестовых наборов.
2.2. Check Система Check [3] предназначена в первую очередь для модульного тестирования ПО в процессе его разработки. Тем не менее, Check можно использовать и для тестирования программных интерфейсов на соответствие стандартам. 68
Check предоставляет разработчику тестов набор макросов и функций для выполнения проверок в тестах, для объединения тестов в наборы, управления выводом результатов и т.д. Тест представляет собой код на языке программирования Си, заключённый между макросами START_TEST и END_TEST. Проверки требований в тестах выполняются с использованием функций fail_unless(проверяющее_выражение, "текст, описывающий ошибку") и fail_if(проверяющее_выражение, "текст, описывающий ошибку"). Как для каждого теста, так и для набора тестов можно задать функции, выполняющие инициализацию и освобождение используемых ресурсов (т.н. checked и unchecked fixtures) К достоинствам системы Check стоит отнести: возможность выполнения каждого теста в отдельном процессе, т.е. изоляция тестов друг от друга и от самой среды Check; автоматическая обработка исключительных ситуаций в тестах; возможность задания максимально допустимого времени выполнения теста; специальные средства для проверки ситуаций, когда выполнение тестируемой функции приводит к посылке сигнала; интеграция системы сборки и запуска тестов с autoconf и automake — широко используемыми средствами автоматизации процесса сборки и установки ПО [4]. Система Check имеет, тем не менее, некоторые недостатки, из-за которых использование её не всегда оправдано. С помощью Check сложно разрабатывать параметризуемые тесты. Часто возникает ситуация, когда некоторую функцию необходимо проверить при разных наборах значений аргументов, а код теста при этом почти не меняется. Логично было бы передавать эти наборы значений аргументов в данный тест как параметры. Но в Check в качестве параметра тесту явно можно передать только его номер, что не очень удобно. Проверки, выполняемые в тесте, никак не привязаны к тем местам в документации по тестируемым функциям, где сформулированы соответствующие требования. Для добавления нового теста в набор необходимо перекомпилировать и код всех остальных тестов из этого набора, что не всегда удобно. Не поддерживается вывод стандартных кодов результата теста, определённых в стандарте на тестирование соответствия POSIX [5].
69
2.3. CUnit Система CUnit [6] может использоваться в тех же случаях, что и Check [3], но обладает в целом более скромными возможностями. Основным недостатком CUnit по сравнению с Check является то, что все тесты, a также система их запуска и сбора результатов, в данном случае выполняются в одном и том же процессе. Т.е. сбой какого-либо из тестов может привести к повреждению области памяти, используемой другими тестами или самой средой CUnit. Также в отличие от Check в CUnit нет защиты от "зависания" тестов (нельзя задать максимально допустимое время работы теста). CUnit имеет и некоторые преимущества по сравнению с Check. Поддержка т.н. fatal и non-fatal assertions. В первом случае, если проверка выявила нарушение требования, выполнение теста прекращается и остальные проверки в нём, таким образом, не выполняются (в Check всё происходит именно так). Если же нарушение требования выявлено в non-fatal assertion, выполнение теста продолжается. Возможно, остальные проверки в данном тесте позволят в таком случае дать разработчику более полную информацию о том, что происходит в тестируемой системе. Это может быть полезно при выяснении причины обнаруженного сбоя. Набор специальных функций и макросов, облегчающих выполнение часто используемых проверок: равенство и неравенство целых чисел, чисел с плавающей точкой, строк и т.д. Поддержка вывода отчётов о выполнении тестов в разных форматах, в том числе пригодных для отображения в web-браузере (xml+xslt). Тем не менее, указанные в предыдущем разделе недостатки системы Check в полной мере относятся и к CUnit. Test Environment Toolkit (TET), о котором речь пойдёт ниже, свободен от некоторых из этих недостатков.
2.4. TET (Test Environment Toolkit) Система TETware (TET, Test Environment Toolkit) достаточно широко используется для тестирования различных программных интерфейсов. Средства TET позволяют запускать разные тесты единым образом и получать отчёт о работе тестов в едином формате [7]. Информация о выполнении теста, включая его результат и сообщения, которые в нём выводятся, попадает в так называемый журнал TET. Основными компонентами TET являются: test case controller (tcc) — этот компонент управляет запуском тестов и сбором выводимой ими информации; интерфейс прикладного программирования (TET API), который нужно использовать в тестах для того, чтобы их можно было 70
выполнять в среде TET. TET API существует для различных языков программирования, в том числе и для С/С++. Наиболее существенными достоинствами TET, на наш взгляд, являются следующие: единая среда для запуска тестов; обработка исключительных ситуаций в тестах (например, segmentation fault) средствами test case controller; общие для всех тестов допустимые коды результата, соответствующие стандарту [5]: PASS, FAIL, UNRESOLVED и т.д., плюс возможность определить дополнительные коды результата тестов; возможность добавлять новые тесты в набор без перекомпиляции существующих тестов (использование т.н. сценариев TET) единый формат отчёта о выполнении тестов (журнала TET). Эти преимущества TET облегчают анализ результатов работы тестов. В частности, программные средства для обработки журнала TET при построении статистики по результатам тестов могут не учитывать специфику проведённых тестов. С другой стороны, средства TET, в основном, автоматизируют выполнение тестов и сбор результатов их работы. TET не предоставляет ни инструментов для автоматизации разработки тестов, ни API для выполнения проверок в тестах. Соответственно, есть несколько причин, из-за которых применение TET "в чистом виде" (без использования каких-либо "надстроек" над ним) не очень удобно. Отсутствие средств для связи проверок, проводимых в тестах, с текстом соответствующего стандарта. Нередко приходится писать тесты, код которых почти одинаков, например, отличаются только параметры вызываемых функций или, допустим, типы элементов используемых массивов и т.п. Возникает естественное желание автоматизировать разработку таких тестов, так, чтобы можно было многократно использовать общие части их кода. К сожалению, в составе TET нет инструментов, позволяющих сделать это. Разработчику тестов необходимо добавить в код определения нужных для запуска в среде TET специфических функций, структур и т.д., что вполне можно было бы сделать и автоматически. Тесты, запускаемые с помощью test case controller, не всегда удобно отлаживать. Как для поиска ошибок в тесте, так и для исследования поведения тестируемой системы в случае сбоя полезно исключить влияние средств TET на работу теста, что существенно упрощает использование программ-отладчиков (например, gdb). 71
Ниже речь пойдёт как раз о "надстройках" над TET (системы GTKVTS и T2C), в которых перечисленные выше недостатки в той или иной степени преодолены.
2.5. Автоматизация разработки TET-совместимых тестов в GTK+-2.0 Verification Test Suite (GTKVTS) Подход, использующийся в GTK+-2.0 Verification Test Suite (GTKVTS) при разработке TET-совместимых тестов, позволяет преодолеть некоторые из описанных выше недостатков TET [8]. Во-первых, в GTKVTS используются т.н. параметризуемые тесты. То есть разработчик пишет шаблон кода тестов на обычном С, отмечая в нём особым образом места, куда впоследствии будут вставлены значения параметров теста. Для каждого такого шаблона может быть задано несколько наборов параметров. Параметром может быть практически что угодно, не только параметр тестируемой функции или её ожидаемое возвращаемое значение. Иногда удобно вынести в качестве параметров типы используемых данных (наподобие template в С++) или даже сам вызов тестируемой функции и т.д. По шаблону кода теста для каждого набора параметров генератор С-кода GTKVTS создаёт отдельную функцию на языке С (см. Рис. 1).
Рис. 1. Генерация С-кода тестов по шаблону. "<%0%>" и "<%1%>" в шаблоне кода теста — места для вставки параметров. Во-вторых, при генерации кода тестов на С средства GTKVTS автоматически вставляют в этот код определения данных, необходимых для работы теста в среде TET, так что разработчику не приходится заботиться об этом самому. 72
Помимо этого, автоматически создаются и make-файлы, необходимые для сборки тестов, а также файлы сценариев TET, что тоже удобно. В GTKVTS также сделана попытка связать выполняемые в тестах проверки с соответствующими фрагментами текста стандарта: в комментариях перед каждым тестом разработчик должен указать текст проверяемых в данном тесте требований. К сожалению, в самом тесте при выполнении проверок этот текст никак не используется, а по трассе теста сложно понять, какие именно требования проверялись и какие из них нарушаются в тестируемой системе. К менее существенным недостаткам средств GTKVTS стоит также отнести отсутствие поддержки отладки теста вне TET, а также специализированность инструментальных средств для использования при разработке тестов только для библиотек из стека GTK+.
2.6. Достоинства и недостатки существующих решений Мы рассмотрели 5 подходов к разработке тестов для программных интерфейсов на языке программирования Си. Достоинства и недостатки каждого из них сведены в Таблице 1. MANUAL Check CUnit TET GTKVTS Параметризация тестов – – – – + Прослеживаемость – – – – – требований стандарта Запуск тестов в – + – – – отдельном процессе Автоматическая обра+ + – + + ботка исключительных ситуаций Ограничение времени – + – + + работы теста Иерархическая + – – – – организация пакетов Удобство отладки тестов + + + – – Переносимость тестов – + + + – Использование стандар– – – + + тных видов вердиктов [5] Таблица 1. Сравнение существующих подходов. Все рассмотренные подходы обладают определёнными достоинствами. Тем не менее, с точки зрения тестирования программных интерфейсов на соответствие требованиям стандарта все они обладают существенным недостатком, заключающемся в отсутствии поддержки прослеживаемости проверок в тестах к требованиям стандарта. Кроме того, ни один из этих 73
инструментов не сочетает в себе все указанные выше достоинства, хотя явных противоречий между ними не существует. При разработке набора инструментов T2C была поставлена задача обеспечить прослеживаемость требований стандарта, реализовав при этом все достоинства существующих решений, представленные в Таблице 1, за исключением иерархической организации пакетов тестов. Данное исключение связано с тем, что возможность иерархической организации пакетов не оказывает существенного влияния на разработку и эксплуатацию тестов на соответствие требованиям стандарта.
Система T2C ("Template-to-Code") 2.7. Общие сведения Система T2C ("Template-to-Code"), позволяет автоматизировать разработку параметризованных тестов, работающих как в среде TET, так и вне её. Код тестов на языке С создается на основе T2C-файла, в котором расположены шаблоны тестов и наборы параметров для них (схема та же, что и для GTKVTS — см. Рис. 1). Фрагмент T2C-файла показан ниже. Тесты, которые будут созданы по шаблону, показанному на Рис. 2, имеют два параметра: TYPE и INDEX. В первом из сгенерированных тестов в качестве TYPE будет использоваться int, в качестве INDEX — 6, во втором — double и 999, соответственно. Также, как и в GTKVTS, в код теста на С автоматически добавляются все необходимые определения данных и функций для выполнения этого теста в среде TET, а также генерируются make-файл и файл сценария TET. Таким образом, инструменты T2C сохраняют основные достоинства системы GTKVTS, но при этом они поддерживают рекомендации по разработке тестов соответствия, сформулированные, в частности, в [9]: составление каталога элементарных требований к тестируемым программным интерфейсам; привязка проверок, выполняемых в тестах, к соответствующим местам в тексте стандарта; оценка качества тестирования в терминах покрытия элементарных требований. В T2C также сделаны следующие усовершенствования по сравнению с GTKVTS. Разработчику тестов предоставляется набор высокоуровневых программных интерфейсов (T2C API), с помощью которых и выполняются проверки в тестах. При этом, если проверка показывает, что некоторое требование стандарта нарушено, в трассу теста (журнал TET) выводится, помимо всего прочего, текст этого требования. 74
Есть возможность создания независимой версии теста на чистом C/C++, не использующей средства TET. Это может быть полезно для отладки самого теста или для более тщательного исследования поведения тестируемой системы в случае отладки ошибки в реализации. Заготовки для T2C-файлов создаются автоматически по размеченному тексту стандарта. В T2C-файле можно указать код, необходимый для инициализации и освобождения ресурсов, общих для всех тестов, код которых будет создан по этому файлу, а также для освобождения ресурсов, выделенных в данном тесте. Поддерживается выполнение тестов в отдельных процессах. Есть возможность задать максимальное допустимое время работы теста. Это полезно в тех случаях, когда какие-то из тестов могут "зависнуть".
Рис. 3. Разработка тестов с помощью системы T2C.
3.8.1. Анализ документации и интерфейсов на группы
Рис. 2. Фрагмент t2c-файла.
2.8. Процесс разработки тестов с помощью системы T2C В этом разделе рассматриваются основные стадии процесса разработки тестов с помощью T2C. 75
разбиение
тестируемых
Прежде, чем приступать к разработке тестов для набора программных интерфейсов, необходимо изучить описание этих интерфейсов в документации, выяснить, что именно предстоит тестировать. При этом стоит разбить интерфейсы на группы, каждая из которых реализует определённую часть функциональности тестируемой системы. Стоит избегать ситуаций, когда, например, для проверки интерфейсов из группы А требуются интерфейсы из группы В ("А зависит от В"), и при этом для тестирования 76
группы В требуются интерфейсы из А (циклическая зависимость групп интерфейсов). Иногда разбиение интерфейсов на группы уже проведено в документации. Например, документация по библиотеке Glib [10] состоит из таких разделов, как "Arrays", "Unicode Manipulation", "Memory Allocation" и т.д. Интерфейсы, описанные в каждом таком разделе, как правило, принадлежат к одной функциональной группе. При разработке тестов для группы интерфейсов создаётся один или более T2C-файлов. На этом этапе удобно создать и необходимую структуру каталогов для тестового набора. Ниже предполагается, что документация по тестируемым интерфейсам является набором html-документов.
3.8.5. Подготовка каталога требований По размеченной документации с помощью инструмента ReqTools, входящего в состав T2C, в web-браузере создаётся каталог требований для данной группы интерфейсов. Этот каталог используется при работе теста: если проверка в тесте показывает, что некоторое требование нарушено, из каталога загружается формулировка этого требования с соответствующим идентификатором и выводится в трассу теста.
3.8.6. Генерация кода тестов, make-файлов и сценариев TET Когда подготовлены тесты в T2C-формате и создан каталог требований, следует запустить генератор кода (T2C Code Generator), который создаст Сфайлы с кодом тестов, make-файлы для их сборки, сценарии TET и т.д.
3.8.2. Разметка требований в документации
3.8.7. Сборка, запуск и отладка тестов
На данном этапе в документации необходимо выделить элементарные требования к тестируемым интерфейсам и присвоить каждому требованию уникальный идентификатор [9]. При необходимости текст требований можно переформулировать для улучшения читаемости. Разметка требований выполняется в html-редакторе KompoZer (www.kompozer.net) с использованием инструмента ReqMarkup, разработанного в рамках проекта OLVER [11] и затем интегрированного в систему T2C.
На данном этапе нужно провести сборку тестового набора, используя makeфайлы, сгенерированные на предыдущем шаге. После чего можно запускать готовые тесты в среде TET или отлаживать какие-либо из них вне TET.
3.8.3. Создание шаблона T2C-файла Когда выделены и размечены все требования для интерфейсов из данной функциональной группы, с помощью ReqMarkup автоматически создаётся заготовка T2C-файла.
3.8.4. Разработка тестов в T2C-формате Этот этап самый важный в процессе создания тестов. На данном шаге разработчику нужно заполнить заготовку T2C-файла, добавляя туда шаблоны кода тестов и наборы параметров для них. Также, если необходимо, в специальных секциях файла следует указывать код инициализации и освобождения ресурсов, общих для всех тестов, код которых будет сгенерирован по этому файлу. При редактировании T2C-файла может оказаться полезным компонент T2C Editor — plug-in для Eclipse IDE. Он обеспечивает удобную навигацию по секциям T2C файла, предоставляет средства для работы с параметрами тестов и т.д.
3. Применение T2C при разработке тестов для LSB Desktop Система T2C использовалась (и используется в настоящее время) при разработке тестов для интерфейсных операций ("интерфейсов") библиотек Linux, входящих в стандарт Linux Standard Base (LSB). C её помощью подготовлены тесты базовой функциональности для следующих библиотек: Glib (libglib-2.0); GModule (libgmodule-2.0); ATK (libatk-1.0); Fontconfig (libfontconfig). В Таблице 2 представлены результаты тестирования. Описания найденных ошибок опубликованы на http://linuxtesting.ru/results/impl_reports. Библиотека Версия Всего Протестировано Найдено интерфейсов ошибок в реализации libatk-1.0 1.19.6 222 222 (100%) 11 libglib-2.0 2.14.0 847 832 (98%) 13 libgmodule-2.0 2.14.0 8 8 (100%) 2 libfontconfig 2.4.2 160 133 (83%) 11 Всего 1237 1195 (97%) 37 Таблица 2. Результаты тестирования LSB библиотек тестами T2C.
77
78
Примечание 1. В столбце "Версия" указывается последняя версия соответствующей библиотеки на момент публикации тестового набора. Количество найденных ошибок указано именно для этой версии. По обнаруженным ошибкам идёт активная работа совместно с разработчиками соответствующих библиотек, так что в более поздних версиях некоторые или даже все из этих ошибок могут быть исправлены. Примечание 2. В столбце "Всего интерфейсов" указано общее количество входящих в LSB интерфейсных операций ("интерфейсов") библиотеки, включая и недокументированные. Практически все документированные интерфейсы были протестированы. Средние затраты на полный цикл разработки теста (от разметки и анализа требований до отлаженного кода теста) для одного интерфейса составили порядка 0,5-1 человеко-дня. Отметим, что интерфейсы в документации по данным библиотекам далеко не всегда описаны подробно. В среднем для каждого интерфейса было выделено 2-3 элементарных требования. Сведения о количестве выделенных элементарных требований к тестируемым интерфейсам и о покрытии этих требований представлены в Таблице 3. Библиотека Выделено Проверено Покрытие по требований требований требованиям, % libatk-1.0 515 497 96% libglib-2.0 2461 2290 93% libgmodule-2.0 21 17 80% libfontconfig 236 177 75% Всего 3233 2981 92% Таблица 3. Выделенные и проверяемые требования для LSB библиотек.
4. Заключение Задача тестирования программных интерфейсов на соответствие документации (в том числе стандартам) является важным звеном в обеспечении качества и интероперабельности программных систем. Для решения этой задачи разрабатываются различные методики и инструменты, позволяющие систематизировать и автоматизировать работу. При этом всегда возникает выбор баланса между качеством тестирования и затратами на разработку соответствующих тестов. Решение здесь обычно принимается директивно из достаточно субъективных соображений. Тем временем, с выбором целевого качества тестирования связан и выбор оптимальной технологии и инструментов, так как для различных уровней цены и качества необходимы разные подходы. Так, например, для глубокого тестирования важных интерфейсов хорошо зарекомендовала себя технология UniTESK [12], 79
которая, однако, требует и соответствующих высоких затрат как на освоение технологии, так и на разработку самих тестов. В данной работе рассмотрена технология T2C, нацеленная на эффективную разработку тестов «среднего уровня» для проверки базовой функциональности программных интерфейсов. В данном случае понятие «средний уровень» соответствует наиболее распространенным представлениям о качестве производственного тестирования, наблюдаемом в большинстве изученных авторами тестовых наборов (например, сертификационные тесты Open Group, сертификационные тесты LSB, тесты OpenPosix и Linux Test Project). T2C позволяет повысить эффективность разработки таких тестов за счет использования следующих основных возможностей, минимизирующих ручной труд на создание окружения и дублирование кода, не принципиального для специфической логики конкретного теста: Автоматическая генерация заготовок тестов на основе каталога требований. Использование именованных параметров в коде тестов, для каждого набора значений которых создается соответствующий экземпляр теста. Высокоуровневое API для использования в коде тестов для осуществления проверок и трассировки. Генерация независимых отдельных тестов в виде самодостаточных простых программ на C/C++, что в практическом плане существенно упрощает отладку тестов и реализации (в отличие от отладки в полной среде запуска TET или подобной). Среда выполнения тестов, созданных по технологии T2C, опирается на широко распространенные средства TETware, что позволяет легко интегрировать тесты в существующие тестовые наборы и окружения управления тестированием и анализа результатов. Кроме того, важной особенностью технологии T2C является систематизированная работа с каталогом требований к проверяемым интерфейсам и обеспечение связи отдельных требований с проверками в тестах и соответствующими сообщениями в отчете о результатах тестирования. Технология T2C была успешно применена в Институте системного программирования РАН в проекте [13] по разработке сертификационных тестов для проверки соответствия библиотек Linux стандарту LSB. В работе представлена статистика по разработанным тестам, найденным ошибкам и трудозатратам, которая позволяет сделать вывод об эффективности данной технологии для заданного сегмента качества разработки тестов программных интерфейсов различных модулей и библиотек. В настоящее время реализована инструментальная поддержка Си и Си++, но нет принципиальных проблем и для других современных языков программирования общего назначения, например C# или Java. Стоит отметить, что важным фактором для успешного применения T2C является наличие достаточно устоявшегося текста 80
требований, так как этап анализа и составления каталога требований в условиях частых изменений и плохого качества описаний может составлять существенную часть трудозатрат. В дальнейшем в T2C предполагается реализовать средства для автоматизации работы с множествами значений параметров тестов. Будет повышена и степень интеграции инструментов T2C со средой разработки Eclipse. Также предполагается исследовать возможности интеграции систем T2C и CTESK. Литература [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13]
Домашняя страница CTESK. http://www.unitesk.com/products/ctesk. The Linux Standard Base. http://www.linux-foundation.org/en/LSB. Домашняя страница Check. http://check.sourceforge.net/doc/check.html/index.html. Домашняя страница Autoconf и Automake. http://www.gnu.org/software/automake/. IEEE.2003.1-1992 IEEE Standard for Information Technology — Test Methods for Measuring Conformance to POSIX — Part 1: System Interfaces. IEEE, New York, NY, USA, 1992. ISBN 1-55937-275-3. Домашняя страница CUnit. http://cunit.sourceforge.net/. TETware User Guide, http://tetworks.opengroup.org/documents/3.7/uguide.pdf. GTKVTS Readme. http://svn.gnome.org/viewvc/gtkvts/trunk/README. В.В.Кулямин, Н.В.Пакулин, О.Л.Петренко, А.А.Сортов, А.В.Хорошилов. Формализация требований на практике. Препринт ИСП РАН, Москва, 2006. Glib Reference Manual, http://www.gtk.org/api/2.6/glib/. Центр верификации ОС Linux. http://linuxtesting.ru/. Домашний сайт UniTESK. http://unitesk.com/. Домашняя страница проекта LSB Infrastructure. http://ispras.linux-foundation.org/.
81
Технология Azov автоматизации массового создания тестов работоспособности Р. С. Зыбин, В. В. Кулямин, А. В. Пономаренко, В. В. Рубанов, Е. С. Чернов {phoenix, kuliamin, susanin, vrub, ches}@ispras.ru Аннотация. В статье рассказывается о технологии Azov, предназначенной для массового создания тестов работоспособности сложных программных систем. Данная технология основана на использовании базы данных со структурированной информацией об операциях тестируемой системы и методики пополнения этой информации за счет уточнения ограничений на типы параметров и результатов операций. Представленные результаты практической апробации технологии Azov демонстрируют ее высокую эффективность при разработке простейших тестов для систем с большим количеством функций.
1. Введение Большая сложность используемых сегодня программных систем и важность решаемых ими задач требуют аккуратной и систематической проверки их корректности в смысле соответствия требованиям и стандартам. Для такой проверки чаще всего используется тестирование — наблюдение за работой системы в ряде специально создаваемых ситуаций и анализ правильности ее работы в каждой такой ситуации с учетом всех существенных аспектов ее поведения. Большинство имеющихся на сегодняшний день методов тестирования требует серьезных затрат для обеспечения некоторых гарантий адекватности или полноты проводимой проверки. То есть, важно, чтобы оценка корректности системы, вынесенная на основе небольшого количества тестовых экспериментов, была верна и по отношению к ее работе во всех возможных ситуациях, которых для практически важных систем бесконечно много. Обычно для этого проводится классификация ситуаций, которые могут возникнуть при работе системы, и организуются тесты на каждый выделенный вид ситуаций, в ходе которых выполняются все необходимые проверки. При автоматизации тестирования чаще всего автоматизируется лишь выполнение тестов. Автоматизация их создания связана с необходимостью формализации как критериев правильности работы тестируемой системы, так 83
и правил классификации тестовых ситуаций и техник формирования входных данных как представителей получаемых классов ситуаций. Поскольку для подавляющего большинства систем все эти критерии и правила заданы неформально, такая автоматизация может потребовать существенных дополнительных затрат на их полную формализацию. В ряде ситуаций такие затраты неоправданны, так как результаты тестирования не должны демонстрировать высокую надежность и полноту проверки выделенных видов тестовых ситуаций. Таково, например, тестирование работоспособности, при котором проверяется только, что основные функции системы выполняются более-менее правильно, т.е. система не разрушается и возвращает результаты, проходящие простейшие проверки на корректность (полная проверка при этом не выполняется). При тестировании работоспособности библиотек проверяют каждую библиотечную операцию (каждый общедоступный метод каждого класса) на наиболее простом сценарии ее использования, проверяя выполнение базовых ограничений на результат работы. Скажем, при тестировании работоспособности реализации функции sin(x) можно вызвать ее со значением параметра 1.0, убедиться, что никаких исключений не возникло, и проверить, что результат лежит на отрезке [–1, 1]. Понятно, что таким образом проверяется только, что данная реализация не делает что-то уж совсем неправильное. Тестирование работоспособности используется, чтобы убедиться в том, что тестируемая система устойчиво работает в простейших сценариях использования. Оно часто проводится для проверки корректности очередной сборки системы, до выполнения более систематического и аккуратного тестирования, поскольку последнее требует значительно больше времени, но бессмысленно, если новая версия тестируемой системы не в состоянии справиться даже с простейшими задачами. Например, аккуратное тестирование функций интерфейса POSIX, управляющих работой потоков, требует разработки достаточно сложных сценариев, в ходе которых создаются многочисленные потоки с различными характеристиками, и в рамках некоторых из этих потоков выполняются какие-то специфические действия. Но использовать такие тесты бессмысленно и неэффективно, если сама функция создания потока содержит ошибку, делающую невозможным создание нового потока при ее обычном использовании. Затраты времени на выполнение сложных тестов и выяснение природы обнаруженной ошибки при этом будут слишком велики, а простейший тест, проверяющий, что новый поток действительно создается и выполняет хотя бы одну простейшую операцию, позволит существенно сократить эти затраты. Таким образом, тестирование работоспособности позволяет экономить усилия, затрачиваемые на поиск и локализацию достаточно грубых ошибок в крупных и сложных программных системах. 84
Как можно автоматизировать создание тестов работоспособности? Казалось бы, что такая автоматизация потребует формализации существенной части требований и критериев выбора тестов как сценариев «нормальной» работы проверяемых операций, для чего необходимо потратить довольно много усилий. Например, функция, предназначенная для открытия файлов, в ходе теста на самом деле должна открывать некоторый файл, а не ограничиваться выдачей кода ошибки. В то же время качество тестов работоспособности довольно низкое, а в результате аналогичных усилий, потраченных на разработку традиционных тестовых вариантов, можно получить более строгие и полные тесты без проведения формализации. Такая оценка в общем случае верна, но иногда возникают дополнительные факторы, позволяющие по новому взглянуть на возможность и необходимость автоматизации тестирования работоспособности. Первый такой фактор — размер и сложность тестируемой системы, определяемые на основании общего количества интерфейсных операций и сложности реализуемых ими функций. Если операций очень много, разработка тестов для них всех вручную становится слишком трудоемкой. Например, в стандарте POSIX 2004 года описано 1123 функции [1], многие из которых решают весьма сложные задачи, а в библиотеке Qt для разработки приложений с графическим интерфейсом пользователя [2], входящей в стандарт Linux Standard Base [3], насчитывается около 10000 общедоступных методов классов и глобальных функций. Другой фактор, который может сделать автоматизацию создания тестов работоспособности более перспективной, — это наличие достаточно полной и хорошо структурированной информации об интерфейсах тестируемой системы. Если такая информация уже есть, она может быть использована для автоматической генерации элементов тестов или их заготовок. Оба этих фактора — большой размер и наличие базы данных с информацией об интерфейсных операциях — имеются у стандарта Linux Standard Base [3] (LSB), включающего несколько отдельных стандартов (POSIX [1], ISO C [4], Filesystem Hierarchy Standard [5] и пр.) и библиотек (Xlib [6], OpenGL [7], GTK+ [8], Qt [2]). В целом в LSB версии 3.1 входит более 30000 функций и методов. Для автоматизации работ по поддержке в актуальном состоянии текста стандарта и набора инструментов для выполнения различных проверок синтаксическая информация обо всех входящих в стандарт интерфейсах помещена в единую базу данных. Это позволило использовать новый подход для автоматизации создания тестов работоспособности для LSB.
2. Технология автоматизации работоспособности
создания
тестов
Для тех случаев, когда интерфейс системы очень велик (состоит из тысяч различных операций) и большая часть информации об элементах этого 85
интерфейса хранится в хорошо структурированном, подходящем для автоматической обработки виде, можно предложить достаточно эффективную технологию автоматизации построения тестов работоспособности, позволяющую создавать их массово, в большом количестве. Такая технология, названная Azov, была разработана в 2007 году в Институте системного программирования РАН. Эта технология существенно использует базу данных с информацией об операциях тестируемой системы и предполагает некоторое уточнение этой информации, в основном сводящееся к уточнению (специализации) типов параметров операций и их результатов. Такое уточнение выполняется вручную и позволяет в дальнейшем полностью автоматически построить корректные входные данные для каждой операции и проверить некоторые свойства ее результата. Технология Azov включает следующие элементы. Методика уточнения данных об интерфейсных операциях. База данных с расширенной информацией о тестируемых операциях. Инструменты, использующиеся для внесения дополнительной информации в базу данных операций. Компоновщик тестов работоспособности, автоматически генерирующий набор тестов для выделенного множества операций по имеющейся в базе информации. Основная идея технологии следующая: информация о типах параметров и результатов тестируемых операций уточняется таким образом, чтобы позволить сгенерировать значения параметров для простейших сценариев нормального использования этих операций и выполнить некоторые (далеко не полные) проверки корректности их результатов. Поскольку одни и те же типы данных в больших системах используются многократно, массовая генерация тестов может привести к существенному снижению трудозатрат на каждый тест.
2.1. Исходные данные и ожидаемые результаты Исходными данными для выполнения работ по описываемой технологии являются база данных, содержащая структурированную информацию об операциях тестируемой системы, и документация на эти операции. Предполагается, что все операции являются функциями языка C или методами классов C++. Структурированность информации об операциях означает, что в этой базе типы параметров и результатов операций должны присутствовать как отдельные сущности, связанные по ссылкам с соответствующими операциями. Более точные требования к исходной информации см. в разделе 2.4.1. Структура базы данных. 86
возможных значений этих типов, а также в виде действий, необходимых для инициализации или уничтожения данных такого типа или же для инициализации или финализации внутренних данных системы, необходимых для нормальной работы операций. В ходе работы применяется методика уточнения данных, представленная ниже. Для заполнения базы данных используются вспомогательные инструменты, например, использующие Web-интерфейс и позволяющие осуществлять навигацию по базе данных, поиск в ней разнообразной дополнительной информации и редактирование уточняемых данных. Перед проведением уточнения необходимо упорядочить тестируемые операции и типы данных, связанные с ними так, чтобы операции, имеющие более сложные типы параметров, шли позже тех, которые имеют простые параметры и могут быть использованы для получения данных более сложных типов. При этом уточнение выполняется от простых операций к более сложным без необходимости часто переключаться на анализ других операций и может естественным образом многократно использовать на поздних этапах информацию, выявленную на ранних.
Документация на тестируемую систему должна содержать достаточно информации, чтобы выявить основные сценарии работы всех интерфейсных операций, включая источники данных для аргументов вызова операции и базовые ограничения на возможные результаты при ее правильной работе. Результатом работы по предлагаемой технологии является набор тестов работоспособности для всех операций тестируемой системы. Для каждой из них в этот набор входит тест, вызывающий операцию в рамках одного из сценариев ее нормальной работы, не приводящей при корректной работе системы к сбоям, исключительным ситуациям или возврату кода ошибки. Этот тест также проверяет некоторые ограничения на результат такого вызова. Все аргументы вызова должны быть построены корректным образом, и, при необходимости, должны быть сделаны предварительные вызовы других операций, инициализирующие необходимые внутренние данные системы, а также итоговое освобождение захваченных ресурсов. Информация в базе данных об интерфейсных операциях и связанных с ними типах пополняется так, чтобы стала возможной автоматическая генерация таких тестов.
2.2. Организация работ по технологии Azov Создаваемые по описываемой технологии тесты представляют собой тестовые варианты, т.е. программы, в рамках которых последовательно выполняются вспомогательные действия по подготовке системы к работе, инициализируются значения параметров для вызова тестируемой операции, выполняется этот вызов и производится финализация системы, т.е. освобождение ресурсов, которые должны быть освобождены по окончании работы. Кроме того, проверяются базовые ограничения на результаты всех выполняемых в тесте вызовов операций. Разработка тестов в рамках технологии Azov организована следующим образом. Разбиение набора операций на функциональные группы. Интерфейс системы делится на группы операций, работающих с одними и теми же внутренними данными, и предоставляющих полный набор действий над ними. Это необходимо, прежде всего, для разбиения работ на достаточно независимые части, каждую из которых может выполнять отдельный разработчик.
Уточнение информации об интерфейсных операциях в базе данных. На этом этапе разработчики анализируют документацию на операции выделенных им групп, определяя условия их нормальной работы и ограничения на их результаты при такой работе. Дополнительная выявляемая при этом информация заносится в базу данных в виде специализированных типов параметров и результатов операций, 87
88
Контроль качества проведенного уточнения. Введенная информация должна пройти проверку на корректность. Такая проверка выполняется как за счет ее просмотра и дополнительного анализа, проводимого другими разработчиками, так и с помощью отладки сгенерированных тестов — они должны компилироваться и собираться, а все проблемы, возникающие при их выполнении должны быть следствием ошибок в тестируемой системе, а не в тестах.
Генерация тестов. Итоговый набор тестов работоспособности генерируется при помощи компоновщика тестов на основе информации из пополненной базы данных.
Выполнение тестов. Сгенерированный набор тестов может представлять собой одну программу или набор программ на языке C или С++. В последнем случае они выполняются в пакетном режиме.
Анализ результатов тестирования. По итогам каждого теста выдается информация либо о его успешном выполнении, либо о нарушении одного из проверяемых ограничений, либо о разрушении тестируемой системы во время его работы. В случае какого-либо нарушения обнаруженная ошибка анализируется разработчиком, и по итогам этого анализа либо фиксируется как
ошибка в тестируемой системе, либо исправляется как ошибка в тесте, после чего тест выполняется снова.
База данных об интерфейсах тестируемой системы
Документация
определенного множества, для соответствующего параметра определяется специализированный тип-перечисление, значениями которого являются элементы этого множества. o
Если при нормальном вызове операции в качестве ее аргумента (или объекта вызова, если эта операция является методом класса) можно использовать лишь значение, являющееся результатом другой операции, то определяется специализированный тип, который одновременно указывается как тип данного аргумента первой операции и как тип результата второй операции.
o
Если при нормальном вызове операции в качестве ее аргумента (или объекта вызова, если эта операция является методом класса) можно использовать лишь значение, для которого предварительно были вызваны некоторые другие операции, для соответствующего параметра определяется специализированный тип, с которым связывается программный код инициализации его значения с помощью необходимых операций.
o
В тех случаях, когда использование аргумента (или объекта вызова) предполагает последующее освобождение ресурсов, с его специализированным типом связывается код финнализации его значения, выполняющий это освобождение.
o
Для описания действий по инициализации и финализации отдельных объектов данного типа иногда требуется ввести вспомогательные операции, которые должны быть один раз определены в каждом тесте, где используется такой тип. Такой код также оформляется как дополнительный атрибут специализированного типа.
o
При уточнении типов параметров иногда несколько параметров объединяются в один абстрактный объект, разные элементы которого используются в качестве их значений, и определяется специализированный тип такого составного объекта. Например, если параметрами операции является указатель на начало строки типа char* и ее длина, можно определить специализированный тип «строка». Вместо первого параметра тогда нужно задавать указатель на первый элемент строки, а вместо второго — результат применения функции strlen() к этому указателю. При создании таких комплексных специализированных типов
Разбиение операций на функциональные группы Уточнение информации об операциях
База данных с уточненной информацией
Контроль качества уточнения
Генерация тестов
Выполнение тестов
Тесты работоспособности
Отчеты о тестировании
Анализ результатов тестирования
Рис. 1. Схема выполнения работ по технологии Azov.
2.3. Методическая основа технологии Методическая основа технологии Azov включает технику уточнения информации об интерфейсных операциях и типах их параметров и результатов в базе данных, а также процедуру автоматической компоновки теста на основе уточненной информации. Уточнение информации об интерфейсных операциях и типах их параметров сводится к следующим действиям. Уточнение (специализация) типов. o
Если при нормальном вызове операции в качестве ее аргумента может быть использовано только значение из 89
90
отдельных
В начало теста вставляется код инициализации для работы данной операции.
o
Если при нормальной работе операции ее результат всегда удовлетворяет некоторым ограничениям, например, возвращается непустой список или возвращается целое число, большее 0, для ее результата определяется специализированный тип, связанный с соответствующим ограничением.
Затем строятся значения всех ее параметров. Для каждого типа аргумента вычисляется значение его порождающего типа (базового типа для указателей, ссылок и пр.), которое затем преобразуется в значение аргумента.
o
При уточнении типов связи между операцией и исходным типом ее параметра или результата в базе данных дополняются аналогичными связями с соответствующим уточненным типом.
определяется код для получения параметров из комплексного объекта.
o
значений
o o
Каждый раз при необходимости введения специализированного типа разработчики анализируют уже имеющиеся специализированные типы, чтобы по возможности использовать повторно уже имеющееся определение типа с нужным набором ограничений.
Определение инициализации и финализации для операций. Если для нормальной работы операции необходимо предварительно выполнить некоторые действия для инициализации внутренних данных системы, и/или провести их финализацию после вызова, то соответствующий вспомогательный код инициализации и финализации привязывается к данной операции.
Определение значений типов параметров. Из возможных типов параметров (в том числе специализированных) выбрасываются примитивные или производные от других типов (указатели, ссылки и пр.) и такие типы, значения которых можно получить только в результате вызовов специальных операций или конструкторов. Кроме того, выбрасываются те типы, любое значение которых может быть использовано как значение параметра этого типа при нормальном вызове произвольной операции с таким параметром. Для каждого из оставшихся типов определяется некоторое значение, которое используется в качестве значения параметров соответствующего типа при вызове операций. Код для получения этого значения заносится в базу данных.
После проведенного уточнения можно применить достаточно простую стратегию компоновки теста, которая позволяет по внесенной в базу данных уточняющей информации автоматически построить тесты работоспособности для всех интерфейсов. Основная процедура построения теста для заданного интерфейса выглядит так. 91
o
Если значение типа определено явно, используется это значение. Если для вычисления значения нужно вызвать другую операцию, конструктор или выполнить инициализирующий код, вставляется вызов этой операции или соответствующий код. При этом значения параметров вызываемых в этом коде операций либо фиксированы, либо вычисляются по аналогичной процедуре. Определения вспомогательных операций, необходимых для построения значений, при этом вставляются в начало теста. Значения других типов строятся автоматически. Для примитивных типов (числа, символы, строки) используются некоторые простые генераторы значений. Значения производных типов — указателей, ссылок, массивов и пр. — строятся из значений их базовых типов. Для перечислений используется первое возможное значение. Объекты структурных типов строятся по их полям, причем поля заполняются по этой же процедуре.
Вставляется вызов аргументами.
Затем вставляется код финализации для всех построенных значений, для которых это необходимо.
В конце вставляется код финализации после работы тестируемой операции.
Для всех вызовов операций, использованных в коде, вставляются проверки ограничений на их результаты, указанные в соответствующих специализированных типах. Кроме того, для всех указателей, которые возникают при вызовах и используются в дальнейшем, проверяется их неравенство NULL.
тестируемой
операции
с
построенными
Несколько более сложные действия выполняются при построении тестов для защищенных методов классов, которые нельзя вызвать из произвольного места. В этом случае генерируется класс, наследующий класс, в котором определен тестируемый метод. В генерируемом классе определяется общедоступный метод, являющийся оберткой унаследованного защищенного 92
метода. В рамках построенного теста вызывается именно этот общедоступный метод. Дополнительная работа необходима для построения значения типа, являющегося абстрактным, неполностью определенным классом. В этом случае, если нет возможности использовать объект одного из наследников этого абстрактного класса, такой класс-наследник генерируется. В нем все абстрактные (чисто виртуальные) методы определяются простейшим образом, и в качестве значения нужного типа используется объект этого классанаследника.
типы, соответственно, BlankingMode, ExposuresMode и ForceMode. Корректные значения для них четко определены в тексте стандарта — это, соответственно, {DontPreferBlanking, PreferBlanking, DefaultBlanking}, {DontAllowExposures, AllowExposures, DefaultExposures} и {Active, Reset}. Четвертый и пятый параметры XGetScreenSaver() являются указателями на типы BlankingMode и ExposuresMode.
2.4. Пример построения тестов по технологии Azov В данном разделе ряд элементов методики построения тестов в рамках описываемой технологии проиллюстрированы на примере функций работы с хранителем экрана (screen saver) в библиотеке Xlib, входящей в LSB. Всего в этой библиотеке 5 таких функций.
int XSetScreenSaver(Display*, int, int, int, int) — устанавливает режим работы хранителя экрана для заданного дисплея. int XGetScreenSaver(Display*, int*, int*, int*, int*) — возвращает текущие параметры работы хранителя экрана для данного дисплея, значения возвращаются по параметрамуказателям, соответствующим параметрам предыдущей функции. int XForceScreenSaver(Display*, int) — активирует или дезактивирует хранитель экрана для заданного дисплея в зависимости от указанного второго параметра. int XActivateScreenSaver(Display*) — активирует хранитель экрана для заданного дисплея. int XResetScreenSaver(Display*) — дезактивирует хранитель экрана для заданного дисплея.
Анализ возможности получения значения типа Display* дает следующие результаты. Всего в LSB упоминается 18 функций, возвращающих значение типа Display*, и две функции, возвращающие ссылку на значение типа Display, из которой можно построить нужный указатель. 6 из этих функций находятся в рамках библиотеки Xlib: XDisplayOfIM(), XDisplayOfOM(), XDisplayOfScreen(), XOpenDisplay(), XcmsDisplayOfCCC(), XkbOpenDisplay(). Остальные функции находятся в других библиотеках — X Toolkit, GTK, Open GL и Qt. Для простейших тестов предпочтительно использовать функции той же библиотеки, поэтому далее анализируются только первые 6 функций.
Документация [6] на функции библиотеки Xlib дает следующее уточнение интерфейса. Второй и третий параметры функции XSetScreenSaver() являются интервалами времени в секундах, определяющими режим работы хранителя экрана. Можно ввести для них тип TimeIntervalInSeconds, определив для него возможное корректное значение 1. Второй и третий параметры XGetScreenSaver() являются указателями на этот же тип.
Четвертый и пятый параметры XSetScreenSaver(), а также второй параметр XForceScreenSaver() имеют на самом деле перечислимые типы, определяющие возможные режимы работы или активации/дезактивации хранителя экрана. Для них можно ввести 93
Возвращаемое всеми функциями значение является кодом ответа, который может сигнализировать о каких-либо проблемах, при этом возвращается значение BadValue. Тип результата в режиме нормальной работы этих функций можно уточнить, назвав его XScreenSaverResult и определив в качестве базового ограничения для его значений неравенство константе BadValue.
94
Из 6 выбранных функций 4 не могут быть использованы, потому что сами косвенно требуют уже иметь некоторое значение типа Display*. Например, XDisplayOfIM() имеет параметр типа XIM, для получения значения которого есть только 2 функции — XOpenIM() и XIMOfIC(). Первая требует на вход Display*, вторая — XIC, который, в свою очередь, может быть создан только функцией XCreateIC(), которая снова требует значения XIM; XDisplayOfScreen() требует параметра типа Screen*, который в Xlib может быть получен только из XDefaultScreenOfDisplay() и XScreenOfDisplay(), а они обе требуют параметра типа Display*. Описание функции XkbOpenDisplay() отсутствует в документации на Xlib.
Остается только функция XOpenDisplay(), которая может быть использована, поскольку требует только параметра типа const char*, могущего принимать значение NULL при штатном использовании этой функции. Таким образом, тесты для рассмотренных функций могут быть построены следующим образом. В качестве значений параметра Display* используется результат вызова XOpenDisplay() с аргументом NULL. В качестве значений интервала времени в секундах используется 1. В качестве значений специализированных типов BlankingMode, ExposuresMode и ForceMode используются, соответственно, значения DontPreferBlanking, DontAllowExposures и Active. Результаты работы всех функций проверяются на равенство BadValue, если результат оказывается равен этому значению, выдается сообщение об ошибке. Кроме того, корректность результатов, возвращаемых по указателям функцией XGetScreenSaver() через ее 4-й и 5-й параметры, можно проверять сравнением их с возможными значениями специализированных перечислимых типов BlankingMode и ExposuresMode. На Рис. 2 представлена схема получения значений параметров и ограничений на результаты для функций из рассмотренного примера. Чтобы можно было разместить ее на странице, на этой схеме опущен специализированный тип ExposuresMode, тип указателей на его значения и связи обоих типов. Данный пример показывает также, что основной источник повышения производительности создания тестов в рамках описываемой технологии — многократное использование специализированных типов. Один раз уточнив тип параметра функции XOpenDisplay(), мы можем получать значения типа Display*, необходимые для многих функций из библиотеки Xlib. В данном примере функций немного, а количество параметров с различным смыслом у них достаточно велико, поэтому снижения трудоемкости создания тестов ожидать не приходится. Если же реализовать многократное использование одних и тех же типов для параметров большого числа функций, становится возможным существенное повышение производительности.
int XSetScreenSaver(
Display*
int
int
int
int
)
BlankingMode TimeIntervalInSeconds
перечислимый значения: DontPreferBlanking PreferBlanking DefaultBlanking
значения: 1 указатель
указатель TimeIntervalInSeconds* BlankingMode*
int XGetScreenSaver(
Display*
int*
int XForceScreenSave(
Display*
int
int XActivateScreenSaver(
Display*
int*
int*
int*
)
)
) ForceMode
int XResetScreenSaver(
Display*
)
перечислимый значения: Active Reset
Display* XScreenSaverResult ограничение: != BadValue
Display*
XOpenDisplay(
const char*
XDisplayID XDisplayID
— специализированные типы
Display*
— исходные типы
значения: NULL
— получение значений исходного типа — получение значений специализированного типа
Рис. 2. Схема получения значений параметров и ограничений на результаты для функций работы с хранителем экрана (не показан тип ExposuresMode). 95
96
)
2.5. Инструментальная поддержка
o
Работа по описываемой технологии поддерживается с помощью следующих инструментов.
База данных с уточненной информацией о тестируемых операциях.
Инструмент для редактирования информации в базе.
Компоновщик тестов, строящий тесты работоспособности по этой информации.
Для редактирования уточненной информации о тестируемых операциях и используемых ими типах данных используется специально разработанный инструмент, имеющий Web-интерфейс и позволяющий как вносить информацию в дополнительные таблицы базы данных, так и выполнять ряд запросов по поиску данных во всех базе и переходы по ссылкам между ее записями. Этот инструмент применяется для создания специализированных типов, внесения дополнительных данных о тестируемых операциях, установления связей между тестируемыми операциями и уже созданными специализированными типами. В частности, он позволяет находить все типы, уточняющие некоторый заданный тип, и тем самым помогает разработчикам повторно использовать уже имеющиеся специализированные типы. Этот же инструмент используется при контроле качества для проверки корректности проведенного уточнения. Для автоматической генерации тестов используется компоновщик тестов, реализующий процедуру построения теста, описанную в разделе 2.3. Методическая основа технологии. Схема основных таблиц базы данных, используемой для хранения как исходной, так и уточненной информации о тестируемых операциях, приведена на Рис. 3. На этом рисунке таблицы исходной базы данных показаны в виде прямоугольников, очерченных тонкими линиями, а таблицы, содержащие уточненную информацию — в виде прямоугольников с жирными границами. Таблицы с уточненной информацией имеют имена, начинающиеся на префикс TG (Test Generation). Из исходной базы данных используется информация о тестируемых операциях, их параметрах и типах параметров и результатов. При этом в базе должны быть следующие поля и связи. Для каждой тестируемой операции: o ее имя; o ссылка на тип результата (если этот тип не равен void); o ссылка на класс, если эта операция — метод класса; 97
для методов классов — флаги, указывающие, что это статический, чисто виртуальный, конструктор, деструктор или обычный метод, а также доступность метода (public, protected).
Рис. 3. Основные таблицы базы данных с информацией о тестируемых операциях.
98
Для каждого параметра одной из тестируемых операций: o ссылка на операцию; o номер параметра;
o
ссылка на тип параметра.
Для каждого типа: o его имя; o разновидность типа — примитивный ли это тип, перечисление, класс, структура, объединение, шаблон, экземпляр шаблона, указатель, алиас (определенный с помощью typedef), константный тип (тип const T для некоторого другого T), тип ссылки, функциональный указатель и др.; o ссылка на базовый тип для указателей, ссылочных типов, константных типов и других аналогичных производных типов.
o
o
Возможные значения специализированных типов описаны в таблице TGSpecTypeValue. Каждая запись о значении содержит код, позволяющий построить это значение.
При уточнении типов может быть уточнен тип результата операции или тип объекта, в котором эта операция вызывается как метод. Для указания этих данных об операциях используется таблица TGInterface и ее поля TGIspecreturn и TGIspecobjecttype. Кроме того использование операции в нормальном режиме может потребовать некоторых действий по инициализации внутренних данных системы и захвату ресурсов до вызова, а также действий по освобождению этих ресурсов после вызова. Код этих действий заносится в поля TGISpreamble и TGISfinalization дополнительной таблицы TGInterfaceSupplement, вместе с ссылкой на уточняемую операцию.
Для поддержки комплексных специализированных типов связь параметра с его специализированным типом опосредуется двумя промежуточными таблицами — таблицей TGParameter, содержащей ссылки на исходные параметры и на получаемые таким образом комплексы, и таблицей комплексов TGParameterProxy. Для каждого комплекса указывается набор параметров, из которых он состоит, ссылка на специализированный тип комплекса и код для получения значений отдельных параметров.
Дополнительные таблицы базы данных содержат следующую информацию. В ходе уточнения создается набор специализированных типов, данные которых должны удовлетворять дополнительным ограничениям. Такие типы описываются в таблице TGSpecType. Для каждого специализированного типа указывается следующее. o
o
o
o
Имя — оно не используется в каком-либо коде и нужно только для удобства ссылок на данный тип, в частности, для поддержки возможности использовать один и тот же специализированный тип для параметров и результатов нескольких операций. Ссылка на исходный тип, который уточняется данным типом. Например, если для параметра, обозначающего интервал времени в секундах в тестируемой системе используется тип int, можно определить специализированный тип «временной интервал, измеряемый в секундах», для которого исходным типом будет int. Ссылка на базовый специализированный тип для указателей, ссылочных типов, константных типов и других аналогичных производных типов. Программный код, определяющий вспомогательные функции для работы с данным специализированным типом. Этот код вставляется в начало каждого теста, в котором фигурируют какие-либо данные этого типа.
Ограничения на специализированный тип описаны в таблице TGSpecTypeConstraint. Каждая запись об ограничении имеет ссылку на соответствующий тип. Ограничения бывают трех видов, вид ограничения заносится в поле TGSTCkind. o
InitCode — набор действий, которые нужно выполнить над объектом или значением исходного, не специализированного 99
типа, чтобы получить объект или значение специализированного типа. FinalCode — набор действий, которые нужно выполнить, чтобы освободить ресурсы, занятые при создании объекта или значения специализированного типа. NormalResult — код, который нужно выполнить, чтобы проверить корректность возвращенного какой-либо операцией значения данного специализированного типа.
3. Использование практике
предложенной
технологии
на
Технология Azov была применена для построения тестов работоспособности библиотеки Qt версии 3 [9], предназначенной для разработки переносимых приложений с графическим интерфейсом пользователя и входящей в состав стандарта Linux Standard Base (LSB, [3]). Стандарт LSB включает 10873 доступных для проверки (public или protected) методов, конструкторов и деструкторов классов Qt 3. Информация о них в структурированном виде представлена в базе данных стандарта, находящейся 100
в открытом доступе [10], как и информация обо всех операциях, входящих в этот стандарт. Однако, часть данных, необходимых для построения корректных тестов, например, сигнатуры чисто виртуальных методов классов, отсутствует в этой базе данных. Эта информация была добавлена в базу при проведении уточнения ее данных. Операции были разбиты на группы по классам, методами которых они являются. Поскольку классов Qt тоже довольно много (около 400), они также были разделены на несколько групп в соответствии с их основной функциональностью. В ходе уточнения было определено 1665 специализированных типов, и для 36 операций были добавлены описания действий по инициализации и финализации. Ряд показателей использования специализированных типов приведен в Табл. 1. Около половины специализированных типов используются повторно, а некоторые — очень много раз. Из таблицы видно также, что примерно в 50% случаев параметры и объекты вызова генерируются автоматически, без использования явно указанных значений специализированных типов. Количество Исходные типы Максимальное количество 513 bool использований (специализированный специализированного типа тип — тип параметров, принимающих значение true) Количество 400 и более 3 bool, int специализированных типов, раз использованных 200-399 раз 5 bool, int, char*, QWidget* 100-199 раз 16 — 10-99 раз 225 — 2-9 раз 556 — Общее количество 1665 — специализированных типов Количество использований специализированных типов как типов параметров или объектов вызова Количество использований всех типов как типов параметров или объектов вызова
11503
—
22757
—
Разработка тестов для Qt вместе с разработкой инструментов, поддерживающих технологию Azov, силами 3-х человек заняла около 4-х месяцев. При этом в начале проекта значительная часть усилий тратилась на разработку и отладку инструментов. На конечной фазе проекта, когда инструменты были уже отлажены, каждый разработчик в день создавал тесты для 80-100 функций, с учетом затрат времени на анализ документации, уточнение данных, генерацию, компиляцию и отладку получаемых тестов. Это существенно больше, чем 3-8 функций в день, обрабатываемых при традиционной ручной разработке тестовых вариантов. Причиной такого повышения производительности являются, прежде всего, широкое и многократное использование специализированных типов и его инструментальная поддержка при проведении уточнения данных о тестируемых операциях. В результате были получены тесты для 10803 функций и методов из 10873. 70 методов (0.6%) не было протестировано по одной из следующих причин. Методы класса QSessionManager не могут быть вызваны обычным образом, поскольку библиотека Qt 3 не позволяет как-либо создать объект этого класса. Такой объект может использоваться только в рамках переопределяемого метода финализации приложения, который вызывается средой Qt3 автоматически в конце работы приложения. Некоторые конструкторы и деструкторы, предназначенные для вызовов только для объектов самого этого класса, а не его наследников, определены для абстрактных классов. Выполнить вызов такого конструктора или деструктора средствами языка C++ не удается.
Несколько методов классов Qt 3 попали в стандарт LSB, хотя не предназначены для использования извне библиотеки.
Для ряда методов отсутствует документация и не удалось подобрать набор значений параметров, для которых такой метод выполнялся бы без разрушения процесса.
При выполнении полученных тестов на одной из реализаций Qt 3 было выявлено около 10 различных ошибок в самой реализации библиотеки, несмотря на то, что все тесты представляют собой простейшие сценарии работы с методами ее классов. Достаточно успешное применение технологии Azov в описанном проекте показывает, что она вполне годится для быстрого создания тестов работоспособности больших промышленных программных систем.
Таблица 1. Показатели использования специализированных типов. 101
102
4. Сопоставление с другими автоматизации создания тестов
подходами
к
Поскольку основной целью разработки технологии Azov являлось массовое создание тестов API для достаточно больших программных систем, использование автоматической генерации тестов в ней является необходимостью. В большинстве подходов к автоматизации тестирования такая генерация отсутствует, поэтому далее Azov рассматриваются в контексте только тех методов, где либо тестовые данные, либо последовательность тестовых воздействий, либо оба этих элемента действительно строятся автоматически на основе некоторой информации. Такие методы можно классифицировать по источникам данных для построения тестовых воздействий или их последовательностей и источникам данных для выполнения проверок корректности в тестах (или тестовых оракулов). При этом многие методы и инструменты попадают сразу в несколько классов, поскольку используют сочетание различных источников для генерации тестов. Можно выделить следующие источники для построения тестовых данных, тестовых воздействий и их последовательностей. Структура тестируемой системы. o Синтаксис сигнатур тестируемых операций и/или разбиение входных данных на области. o Структура типов данных. o Связи по типам — использование одних и тех же типов данных для параметров и результатов тестируемых операций и др. o Структура кода (прежде всего, потока управления) операций.
Описание функциональности тестируемых операций. o Ограничения, связанные с типами данных. o Ограничения, связанные с каждой операцией отдельно от остальных. o Ограничения, связывающие поведение нескольких операций.
Для выполняемых в тестах проверок исходной информацией всегда служит либо описание функциональности в одном из перечисленных выше видов, либо предположения о том, что корректное поведение системы не должно включать создание исключительных ситуаций, посылку сигналов, разрушение процессов и другие специфические действия, обычно (но не всегда!) сигнализирующих об ошибках.
Источник данных для проверки Источник тестовых воздействий Синтаксис сигнатур и/или разбиение входных данных
Структура типов
Требование отсутствия Нет проверок исключений и серьезных сбоев Методы и инструменты генерации тестов на основе покрывающих наборов [11] OTK/Pinery [12,13]
Ограни-чения на одну операцию
Совместные ограничения для нескольких операций
Инструменты Parasoft (Jtest, C++test, .TEST [14]) Ограничения в виде утверждений, постусловий и инвариантов JCrasher [15] Azov
Связи по типам
Структура потока управления
Ограничения типов данных
Ограничения на одну операцию
Методы генерации структурных тестов на основе генетических алгоритмов [16-18] CodeScroll API Tester, Suresoft Technologies [19] Методы генерации структурных тестов с использованием разрешения ограничений (constraint solving) [20,21] Pinery [13]
Azov Ограничения в виде инвариантов данных типа и действий по корректному построению этих данных
T-VEC Test Vector Generation System [22] Ограничения в виде постусловий и инвариантов Lurette [23]
Совместные ограничения для нескольких операций
Методы и инструменты генерации тестов на основе автоматных моделей, включая Stateflow, Statecharts, SDL, LOTOS, Lustre или специализированные нотации [24,25] UniTESK [26]
Таблица 2. Классификация подходов к автоматическому построению тестов. Классификация имеющихся исследовательских методов,
103
Ограничения типов данных
104
на рынке инструментов, а также ряда использующих автоматическую генерацию
так и определением некоторых шаблонов построения таких значений, в свою очередь имеющих некоторые параметры. Во втором случае для построения конкретного значения по шаблону с параметрами можно использовать процедуру выбора значений параметров, реализуемую компоновщиком тестов.
тестов, на основе выполняемых проверок и видов данных, используемых для построения тестов, показана в Табл. 2. Технология Azov в этой таблице попадает сразу в несколько клеток, поскольку сочетает при генерации тестов использование структуры типов, связей по типам, действий по инициализации данных типа и (иногда) специфических действий по инициализации системы перед использованием определенной операции. Для проверок используются ограничения типов результатов и фактор отсутствия сбоев при вызове тестируемой операции. Заметим, что кроме технологии Azov связи по типам используются при генерации тестов только рядом методов построения структурных тестов на основе генетических алгоритмов [19-21].
o
Добавление различных сценариев использования. При тестировании работоспособности операции вызываются в наиболее простых сценариях их использования. Однако даже часто возникающих сценариев использования операции бывает несколько, иногда для граничных значений параметров операция должна демонстрировать специфическое поведение. Кроме того, большинство операций должно правильно реагировать на некорректно заданные параметры. Для добавления поддержки различных сценариев использования необходимо уметь задавать набор сценариев или режимов работы операции и для каждого режима определять соответствующие специализированные типы параметров и специфические ограничения на результаты работы.
o
Построение тестовых последовательностей. Для операций, поведение которых зависит от внутреннего состояния системы, можно увеличить качество и количество создаваемых тестов за счет автоматического добавления перед вызовом операции обращений к другим операциям, изменяющих те внутренние данные, от которых зависит работа этой операции.
5. Дальнейшее развитие технологии Дальнейшее развитие технологии Azov возможно по нескольким направлениям. Расширение возможностей повторного использования. В данный момент специализированные типы представляют собой практически единственный вид элементов технологии, которые можно использовать многократно. Для расширения таких возможностей можно определить механизмы, позволяющие повторно использовать ограничения типов, позволив таким образом создавать типы, уточняющие другие специализированные типы и наследующие все их ограничения.
Увеличение качества и полноты тестов. Тестирование работоспособности предполагает однократный вызов каждой из тестируемых операций и проверку только некоторых базовых ограничений. Однако предложенную технологию достаточно просто расширить так, чтобы создаваемые тесты обладали большей полнотой и более аккуратно проверяли тестируемую систему. o
o
Добавление дополнительных ограничений. В настоящий момент проверяются ограничения, применимые только при одном из простейших сценариев использования. Можно ввести более сложные проверки за счет введения дополнительных ограничений и условий на входные параметры, при которых эти ограничения должны проверяться. Использование нескольких наборов параметров. Для более тщательного тестирования можно использовать не по одному значению каждого параметра, а строить различные их наборы, перебирая некоторое множество значений для каждого параметра. Множества значений типов можно указывать как простым перечислением этих значений в базе, 105
106
Создание тестов совместимости. Описанная технология может использоваться для создания тестов совместимости нескольких библиотек, если объекты вызова и аргументы вызова тестируемых операций из одной библиотеки в тестах получать как результаты вызовов операций из других библиотек.
Использование представленной методики уточнения при быстрой разработке программного обеспечения. Методику уточнения информации об операциях можно использовать при разработке программного обеспечения не только для создания тестов. Поскольку база данных с исходной информацией об операциях системы может быть достаточно просто получена как побочный результат компиляции, эта методика может быть основой контроля качества, анализа и уточнения проектных решений, создания проектной документации при быстрой разработке программных систем. Работа может быть построена на основе ежедневных сборок: база
данных с информацией о системе, собранной в предыдущий вечер наутро поступает для уточнения и анализа. Выявленная при таком анализе за день информация о дополнительных ограничениях на типы параметров и результатов далее согласовывается с проектировщиками и может использоваться для уточнения и исправления ошибок проектирования и для создания проектной документации. При этом, однако, важно иметь специальные механизмы распознавания и интеграции изменений базы по сравнению с предыдущей версией.
6. Заключение Автоматизация создания тестов обычно основана на формализации большого количества правил и критериев, которые управляют ручной разработкой тестов, оставаясь несформулированными явно. Такая формализация чаще всего требует серьезных трудозатрат, но окупается за счет полноты и качества получаемых в результате тестов. Однако возможно автоматизировать и создание гораздо менее аккуратных тестов, проверяющих только базовую работоспособность системы. В данной статье представлена технология Azov автоматизации разработки тестов работоспособности, созданная в Институт системного программирования РАН. Для ее применимости достаточно иметь базу данных, где информация об операциях системы представлена в хорошо структурированном виде, и документацию с описанием базовой функциональности этих операций. Можно заметить, что любой компилятор в принципе способен выдать такую базу данных в качестве побочного результата своей работы. Апробация технологии на библиотеке Qt для разработки приложений с графическим пользовательским интерфейсом, включающей более 10000 операций, показала, что технология вполне успешно и достаточно эффективно справляется с возложенными на нее задачами. Хотя инструменты, поддерживающие работу по описываемой технологии, создавались и дорабатывались непосредственно в ходе этого проекта, были достигнуты весьма высокие показатели производительности. Технология Azov обладает также серьезным потенциалом для дальнейшего развития и может быть использована как для создания более качественных тестов, так и для совсем других видов тестирования, например, тестирования совместимости и возможности взаимодействия различных программных систем.
[2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] [16] [17] [18] [19] [20] [21] [22] [23] [24] [25] [26]
Литература [1] IEEE 1003.1-2004. Information Technology — Portable Operating System Interface (POSIX). New York: IEEE, 2004.
107
108
http://doc.trolltech.com/4.2/index.html. http://www.linuxbase.org. ISO/IEC 9899:1999. Programming Languages — C. Geneve: ISO, 1999. http://www.pathname.com/fhs/. Xlib — C Language X Interface. X Consortium Standard. http://refspecs.freestandards.org/X11/xlib.pdf. http://www.opengl.org. http://www.gtk.org. http://doc.trolltech.com/3.3/index.html. http://www.linux-foundation.org/navigator/commons/welcome.php. http://www.pairwise.org/tools.asp. С. В. Зеленов, С. А. Зеленова, А. С. Косачев, А. К. Петренко. Генерация тестов для компиляторов и других текстовых процессоров. Программирование, 29(2):59–69, 2003. А. В. Демаков, С. В. Зеленов, С. А. Зеленова. Генерация тестовых данных сложной структуры с учетом контекстных ограничений. Труды ИСП РАН, 9:83–96, 2006. http://www.parasoft.com/jsp/products.jsp. C. Csallner, Y. Smaragdakis. JCrasher: and Automatic Robustness Tester for Java. Software — Practice & Experience, 34(11):1025–1050, 2004. R. Ferguson, B. Korel. The Chaning Approach for Software Test Data Generation. ACM Transactions on Software Engineering Methodology, 5(1):63–86, 1996. R. P. Pargas, M. J. Harrold, R. Peck. Test-data Generation Using Genetic Algorithms. Software Testing. Verification & Reliability, 9(4):263–282, 1999. A. Seesing, H.-G. Gross. A Genetic Programming Approach to Automated Test Generation for Object-Oriented Software. Intl. Trans. on System Science and Applications, 1(2):127–134, 2006. http://www.suresofttech.com/eng/main/product/api.asp. B. Korel. Automated Test Data Generation for Programs with Procedures. In Proc. of ISSTA 1996, pp. 209–215. A. Gotlieb, B. Botella, M. Rueher. Automatic Test Data Generation Using Constraint Solving Techniques, ACM SIGSOFT Software Engineering Notes, 23(2):53–62, March 1998. http://www.t-vec.com/solutions/tvec.php. P. Raymond, D. Weber, X. Nicollin, N. Halbwachs. Automatic testing of reactive systems. In Proc. of 19-th IEEE Real-Time Systems Symposium, 1998. A. Hartman. Model Based Test Generation Tools. 2002. http://www.agedis.de/documents/ModelBasedTestGenerationTools.pdf. M. Broy, B. Jonsson, J.-P. Katoen, M. Leucker, A. Pretschner, eds. Model-Based Testing of Reactive Systems. Advanced Lectures. LNCS 3472, Springer, 2005. В. В. Кулямин, А. К. Петренко, А. С. Косачев, И. Б. Бурдонов. Подход UniTesK к разработке тестов. Программирование, 29(6):25–43, 2003.
Расширение описаний сигнатур операций для автоматической генерации тестов Р. С. Зыбин, А. В. Пономаренко, В. В. Рубанов, Е. С. Чернов {phoenix, susanin, vrub, ches}@ispras.ru Аннотация. В статье рассматривается задача расширения стандартной информации, извлекаемой из сигнатур программных интерфейсов (операций), для обеспечения возможности автоматической генерации тестов, вызывающих цепочки таких операций с корректными параметрами и в корректном окружении. Эта задача актуальна при тестировании интерфейсов систем с большим числом операций (больше 1000) в условиях ограниченных ресурсов на создание тестов. Для описания расширенных данных вводятся так называемые специализированные типы, уточняющие семантику типов объектов, возвращаемых значений и параметров. С их помощью можно дополнить исходное описание сигнатур операций, задав недостающую для эффективной генерации тестов информацию. Рассматриваются различные виды и свойства специализированных типов. Отдельно обсуждаются аспекты, ускоряющие ввод необходимых дополнительных данных для групп операций. Приводится пример реализации хранения расширенной информации об операциях в базе данных стандарта Linux Standard Base.
1. Введение При тестировании программных систем, состоящих из большого числа интерфейсных операций (API) в условиях ограниченных ресурсов на создание тестов, необходимо использование автоматизированных методов и инструментов, позволяющих быстро и в большом количестве создавать тесты, проверяющие по крайней мере минимальную функциональность. Сами тесты при этом могут не проверять полную корректность соответствующих операций, но могут выявить случаи разрушения системы, а при удачном прохождении теста — гарантировать ее минимальную работоспособность. Чтобы создать хоть и простейший, но корректный тест автоматически, необходимо иметь формальное описание правил вызова тестируемой операции. Сигнатуру операции (имя, типы принимаемых параметров и возвращаемого значения) можно полностью автоматически получить разными способами: из заголовочных или бинарных файлов или из существующих баз данных. Однако в большинстве случаев типы параметров и возвращаемых 109
значений в сигнатуре слишком общие. Для примера рассмотрим функцию arcsin(double). Сигнатура позволяет вызывать ее на любом параметре типа double. Однако из математических особенностей этой функции следует, что значение фактического параметра по абсолютному значению не должно по модулю превосходить 1. Иначе возникнет исключительная ситуация, и такой вызов нельзя будет считать нормальным (в рамках тестирования минимальной функциональности). Поэтому для обеспечения корректного формирования значения параметра при вызове такой операции необходимо задать или формальное ограничение (x >= –1 && x <= 1) или просто конкретное значение, удовлетворяющее этому ограничению (например, x = 0,5). Для сложных типов данных задание правил конструирования корректного значения может приобретать более сложный вид, например для конструирования некоторых параметров необходимо вызывать другие функции или даже целые цепочки операций. Заметим, что, кроме описания свойств параметров, для целей генерации тестов следует описать еще и условие на возвращаемое значение и на объект (в случае, когда целевая операция — метод класса). В данной работе рассматриваются необходимые расширения содержащейся в сигнатурах стандартной информации, которые позволяют полностью автоматически сформировать цепочку инициализации корректного окружения и параметров для вызова целевой операции и проверить базовую корректность ее выполнения. Центральным элементом этой расширенной информации являются специализированные типы, которые содержат в себе уточнение семантики стандартных типов объектов, параметров и возвращаемых значений и позволяют автоматически конструировать соответствующие корректные значения. Один специализированный тип может привязываться к различным параметрам и возвращаемым значениям различных операций, то есть переиспользоваться. Информация из сигнатур, дополненная специализированными типами, привязанными к конкретным объектам, параметрам и возвращаемым значениям определенного набора операций позволяет полностью автоматически строить тесты работоспособности для этих операций. Однако сами специализированные типы необходимо описывать и привязывать к операциям вручную. Поэтому очень важно обеспечить эффективное создание новых и переиспользование уже созданных специализированных типов для разных операций. В связи с этим вводятся определенные механизмы для специализированных типов, позволяющие уменьшить количество создаваемых типов и увеличить скорость их привязки к операциям. Статья состоит из четырех разделов. В первом разделе вводятся термины для обозначения атрибутов (свойств) специализированных типов. Во втором разделе рассматриваются основные атрибуты, необходимые для автоматической генерации тестов, а в третьем — свойства и механизмы, позволяющие ускорить создание тестов для большого числа операций. В 110
четвертой части описывается конкретная реализация механизма хранения расширенной информации об операциях и специализированных типах на примере расширения базы данных стандарта LSB [1,2].
2. Специализированные типы Для задания специализированного типа, нужно перечислить значения всех его атрибутов. Ниже приведено краткое описание каждого из них. Название (name) — в нем отражена основная цель создания специализированного типа. Исходный тип даных (type) — тип данных, который специализированный тип уточняет. Базовый тип (base type) — специализированный тип, от которого данный специализированный тип унаследован. Вид (kind) — специализированный тип может быть нескольких видов: обычный, единожды используемый, тип по умолчанию для параметров, тип по умолчанию для возвращаемого значения. Значение (value) — им инициализируется параметр, использующий данный специализированный тип. Ограничение (constraint) — хранит условие на возвращаемое значение. Инициализирующий и финализирующий код (init/final code) — программный код, который необходимо вставить соответственно до и после вызова операции. Вспомогательный код (auxiliary code) — программный код, который оформляется в виде набора функций и используется для инициализации объектов. Кроме того, в нем имеется возможность переиспользовать уже созданные специализированные типы. Прокси-значения (proxy-value) — используются в случае зависимости параметров друг от друга. В этом случае один специализированный тип уточняет сразу несколько параметров. Все зависимости хранятся в прокси-значениях, каждое из которых используется одним параметром и может иметь ссылку на другие параметры. Атрибуты Название и Исходный тип данных отличают специализированные типы друг от друга, т.е. для одного типа данных не может быть два разных специализированных типа с одинаковым названием. Атрибут Базовый тип определяет отношение наследования между специализированными типами. Никаких других отношений между ними не предусмотрено. Для описания атрибутов Значение, Ограничение, Инициализирующий, Финализирующий и вспомогательный код должен использоваться язык 111
программирования. В данной статье в качестве примера используется язык С++. Как уже отмечалось, для задания специализированного типа нужно перечислить значения всех его атрибутов. Далее в статье, при определении конкретного типа, будут перечисляться все его непустые атрибуты. Исключение составляет его название, которое, как правило, указываться не будет, т.к. не является существенным. Каждый из атрибутов будет более подробно описан в следующих разделах.
3. Главные атрибуты специализированного типа В данном разделе описаны атрибуты специализированного типа (в дополнение к описанным выше атрибутам Название, Исходный и Базовый тип), позволяющие хранить необходимую для автоматического создания теста информацию.
3.1. Значение специализированного типа Наиболее простым способом выполнить ограничения на входное значение параметра является явное указание значения, которое ему будет присвоено. Именно оно хранится в атрибуте “Значение” специализированного типа. Приведем несколько примеров специализированных типов с допустимыми значениями: 1) value = 32 2) value = “Test string” 3) value = true. В сгенерированной программе соответствуют строки: Par_N = 32; Par_N = "Test string"; Par_N = true;
этим
специализированным
типам
Где N — номер параметра, который использует специализированный тип с соответствующим значением. Кроме того, значением может быть функция, определенная во вспомогательном коде (см. раздел 3.4) специализированного типа, например: value = create_param(). Ей соответствует строка: Par_N = create_param(); Если для инициализации параметра нужно знать имя переменной объекта, то к ней можно обратиться с помощью специальной конструкции: $obj. В сгенерированной программе $obj заменится именем переменной объекта. Например, для значения $obj.first() будет создан следующий код: SomeClass Obj; 112
… OtherClass Par_N = Obj.first(); … Обычно для инициализации встроенных типов — int, float, bool, const char* и т.д. — указывается конкретное значение (10, 5.25, false, “some string”), а для инициализации объектов — либо конструктор, либо функция, определенная во вспомогательном коде специализированного типа. Как правило, для параметра существует не единственное значение, с которым можно вызывать операцию, поэтому у одного специализированного типа может существовать несколько значений. Это приведет к созданию нескольких тестов для одной операции.
3.2. Ограничения специализированного типа Кроме входных значений для параметра, специализированный тип может еще хранить некоторые ограничения на параметр. В качестве такого ограничения выступает логическое выражение, которое является истинным при правильной работе целевой операции. Как и для значения специализированного типа, для описания ограничений используются специальные конструкции: $obj — заменяется именем переменной объекта, $0 — заменяется именем переменной параметра, для которого задан этот специализированный тип. Вот несколько примеров такого вида ограничения: “$0 == 1”, “$0 != NULL”, “$obj.isEmpty() == false”. В сгенерированных программах эти ограничения будут использоваться оператором условия для определения правильности работы интерфейса. SomeClass Obj; … Obj.someMethod(Par_1, Par_2, …); if (!(Par_1 == 1)) { /* вывод сообщения о нарушении ограничения $0 == 1 */ } if (!(Par_2 != NULL)) { /* вывод сообщения о нарушении ограничения $0 != NULL */ } if (!(Obj.isEmpty() == false)) { /* вывод сообщения о нарушении ограничения $obj.isEmpty() == false */ } В данном примере специализированный тип с первым и вторым ограничением был использован первым (Par_1) и вторым (Par_2) параметром операции, а 113
специализированный тип с последним ограничением — на объект. Если результат одного из логических выражений будет “ложь”, то будет выдано сообщение о нарушении соответствующего ограничения.
3.3. Инициализирующий и финализирующий код Инициализирующий код соответствует программному коду, который будет вставлен до вызова целевого интерфейса, а финализирующий — после. Также как и для ограничений, для их описания используются $0 и $obj. Например, в инициализирующем коде можно заполнить список 10-ю элементами: for (int i = 0; i < 10; i++) { $0.append(i); } Тогда в сгенерированной программе будет следующий код: … SomeClass Obj; // Init code for (int i = 0; i < 10; i++) { Par_1.append(i); } // Call of target interface Obj.someMethod(Par_1,…); … В финализирующем коде можно, например, закрыть файл: close($0); Соответствующий сгенерированный код: … Par_1 = fopen("test.cpp", "r+"); Obj.someMethod(Par_1); // Final code close(Par_1); …
3.4. Вспомогательный код Набор дополнительных функций, которые можно затем использовать в других атрибутах специализированного типа (включая Значение), оформляется в атрибуте Вспомогательный код. При генерации теста, определения таких функций помещаются в начале файла с исходными кодами теста, в котором происходит вызов целевых операций, использующих данный тип. При этом, в отличие от инициализирующего кода, вспомогательный код не дублируется для различных параметров в рамках одного теста. Кроме того, в нем возможно использование дополнительных конструкций, указывающих генератору 114
проинициализировать переменную определенного типа или вызвать операцию со всеми автоматически проинициализированными корректными параметрами: $(type) — указание генератору создать переменную типа ‘type’; $[function] — указание генератору проинициализировать и вызвать соответствующую операцию. Эти конструкции позволяют существенно снизить время создания специализированного типа и количество ошибок, т.к. уменьшается объем кода, который должен написать разработчик. Кроме того, не нужно отвлекаться на то, чтобы узнать, как правильно инициализировать определенный тип данных или с какими параметрами запустить операцию. Не думать об этом позволяет переиспользование информации, занесенной ранее для этих операций или типов данных. Кроме того, указание таких ссылок, а не конкретного программного кода, позволяет менять способ инициализации операции или класса только в одном месте, а не во всех специализированных типах, которые их используют. Конструкции $0 и $obj здесь уже использовать нельзя, т.к. вспомогательный код не привязан к конкретному параметру. Однако при необходимости можно получить доступ к необходимым параметрам, передав их в качестве параметров определяемой во вспомогательном коде функции во время ее вызова в значении специализированного типа или в инициализирующем коде. Часто для корректной инициализации объекта нужно вызвать несколько его set-методов. Они могут определять как простые свойства объекта (активный/неактивный, изменяемый/неизменяемый в размерах и т.д.), так и сложные, требующие инициализации других объектов (цвет, шрифт, иконка, курсор и т.д.). Например, можно написать следующий вспомогательный код и для значения соответствующего специализированного типа использовать просто строку create_SomeClass(): SomeClass* create_SomeClass() { SomeClass* Obj = new SomeClass(); Obj->setEnabled(true); Obj->setFont( $(QFont *) ); Obj->setIconSet( $(QIconSet) ); return Obj; } Здесь в методах setFont и setIconSet использовалась конструкция, указывающая генератору проинициализировать корректные значения типов QFont* и QIconSet. Если созданный специализированный тип задать для объекта класса ‘SomeClass’, то получим следующую программу: 115
static const char * const XPM[]={"16 15 8 1", "a c #cec6bd"}; SomeClass* create_SomeClass(SomeClass* Obj) { Obj->setEnabled(true); Obj->setFont(new QFont("Times", 10, Bold)); QPixmap Par_1_1(XPM); QIconSet Par_1(Par_1_1); Obj->setIconSet(Par_1); return Obj; }; int main() { SomeClass* Obj = create_SomeClass(); // Call of target interface … } Конструкция $(QFont *) развернулась в строку "QFont("Times", 10, Bold)", представляющую собой вызов конструктора класса QFont, для которого параметры были уточнены специализированными типами со значениями соответственно “Times”, 10 и “Bold”. Конструкция $(QIconSet) развернулась в код из нескольких строчек, создающих объект QPixmap с помощью глобальной переменной XPM, определенной во вспомогательном коде специализированного типа, наложенного на параметр конструктора класса QPixmap. При написании этого кода вручную пришлось бы разбираться, как можно проинициализировать объекты классов QFont, QIconSet и типы, от которых они зависят (в данном случае QPixmap). Указания генератору создать объекты этих классов автоматически помогают сократить затраты времени и усилий на создание тестов для зависящих от них операций и избежать возникающих при этом ошибок.
3.5. Прокси-значения для комплексных специализированных типов Обычно специализированный тип несет в себе дополнительное описание лишь одного параметра. При этом использующую только такие типы программутест можно разбить на независимые блоки кода, каждый из которых инициализирует свой параметр. Однако на практике существуют зависимости описаний параметров друг от друга, т.е. в строках кода для инициализации одного параметра необходимо использовать значение другого параметра. Типичным примером такого рода зависимости является ситуация, когда один параметр является строкой, а другой равен длине этой строки. В этом случае описание первого и второго параметра нельзя разделить на независимые строки кода: 116
Par_1 = "Some String"; Par_2 = strlen( Par_1 ); Для разрешения таких ситуаций вводятся комплексные специализированные типы. Фактически, они содержат в себе описание для нескольких параметров. В расширенной сигнатуре операции зависимые параметры объединяются в один, имеющий соответствующий комплексный специализированный тип (см. Рис. 1). Все зависимости между исходными параметрами остаются только внутри специализированного типа, а зависимостей между параметрами в расширенном описании операции нет. При этом конечная программа представляет собой объединение независимых описаний различных групп параметров, где специализированным типам соответствуют отдельные группы. При этом некоторые из них описывают зависимости между параметрами, как и блок кода, приведенный выше.
Вместо $1 можно использовать другие конструкции, но в любом случае при описании комплексных типов должен использоваться механизм указания ссылок на другие параметры. В данном случае $1 выступает в виде ссылки на первый параметр. Кроме того, могут быть использованы конструкции, которые, так же как и в обычном специализированном типе, заменяются именем самого параметра и именем переменной объекта (например, $0 и $obj соответственно).
4. Средства упрощения создания тестов Главным преимуществом автоматического создания простейших тестов являются небольшие затраты труда на один тест. Наибольший вклад в общую трудоемкость создания тестов вносят ручные действия, а именно: создание специализированных типов и расширение описаний операций (привязка специализированных типов к параметрам операции). Эти действия выполняет разработчик с помощью специальных инструментов редактирования информации о специализированных типах, имеющих графический интерфейс. Чтобы снизить усилия на обработку каждой операции, в специализированные типы были введены некоторые механизмы, позволяющие как снизить количество создаваемых типов, так и облегчить взаимодействие разработчика с графическим интерфейсом. В данном разделе приведено их подробное описание.
4.1. Наследование специализированных типов
Рис. 1. Расширение сигнатуры операции Чтобы иметь возможность объединять параметры одним описанием, специализированный тип имеет атрибуты “Прокси-значение”. Количество этих значений равно количеству объединяемых параметров. Каждое проксизначение хранит строку инициализации для соответствующего параметра. В примере, описанном выше, будет создан комплексный специализированный тип, имеющий два прокси-значения: для первого параметра — “$1 = "Some string"”, а для второго — “strlen($1)”. При их описании была использована специальная конструкция — $1. Параметру, которому соответствует значение “$1 = "Some string"”, будет присвоено значение “Some string”, а $1 будет везде заменяться именем переменной этого параметра. Поэтому вместо второго параметра, которому соответствует значение “strlen($1)”, в программе будет строка “Par_2 = strlen( Par_1 )”. 117
При создании специализированного типа на основе некоторого исходного может возникнуть потребность в использовании некоторых атрибутов другого, уже созданного специализированного типа для родственного исходного типа. Повторно используемыми атрибутами могут являться значения, ограничения, инициализирующий, финализирующий и вспомогательный код. Чтобы была возможность воспользоваться атрибутами другого специализированного типа, был введен механизм наследования. При наследовании в атрибуте “Базовый тип” типа-потомка указывается типродитель. При использовании типа-потомка отсутствующие у него атрибуты берутся у типа-родителя. Таким образом, специализированный тип можно создать достаточно быстро — просто указать его родителя, не указывая при этом никаких других его свойств. В этом случае все атрибуты будут браться у типа-родителя. Такое наследование полезно при создании специализированных типов для встроенных типов данных (int, char, long, void* и т.д.) для проверки возвращаемого значения. Например, чтобы иметь специализированный тип для проверки на NULL, нужно создать специализированный тип, основанный на void*, в ограничении которого написать условие “$0 != NULL”, а другие специализированные типы ненулевых указателей можно от него наследовать. 118
Кроме простого наследования с заимствованием всех атрибутов у типародителя, можно также использовать более сложный вид наследования — с переопределением некоторых атрибутов в типе-потомке. Такое наследование типично для инициализации объектов, к примеру, когда значение и ограничения специализированного типа-родителя нужно сохранить, а инициализирующий код — немного изменить. Такие механизмы как множественное (от нескольких специализированных типов) и многоуровневое (от потомка) наследования также могут быть реализованы, однако потребность в них возникает крайне редко, а их существование может привести к достаточно запутанной зависимости между специализированными типами.
4.2. Неявное наследование специализированных типов При расширении описания операции обычный тип параметра заменяется на специализированный. В других операциях параметр такого же типа может быть заменен этим же специализированным типом. То, какой тип данных может быть заменен специализированным типом, указано у последнего в атрибуте “исходный тип данных”. Однако, формально, типы int, int*, const int& и т.д. являются разными. Поэтому для инициализации параметров этих типов, например, единицей, следовало бы создать специализированные типы для каждого параметра. При этом, хотя потребуются значительные усилия для их создания, их смысловое содержание будет одинаковым: все должны инициализировать свой параметр единицей. Одним из решений этой проблемы является использование механизма наследования. Например, можно создать специализированный тип для int, а для const int& создать его наследника. В этом случае создание нового типа займет меньше времени (т.к. основные атрибуты наследуются от родителя), но все равно потребуется создавать новый тип. Кроме того, для типа int* уже нельзя просто наследовать значение специализированного типа для int, т.к. у первого строка инициализации “new int(1)”, а у второго — “1”. Чтобы избежать описанной проблемы, нужно иметь возможность использовать специализированный тип для параметров, типы которых являются производными от того же типа, на котором основан исходный тип этого специализированного типа. Тогда для параметра типа int* можно будет указать специализированный тип, основанный на int. Тип переменной, генерируемой при инициализации параметра, при этом будет совпадать с исходным типом специализированного типа. При использовании этой переменной в качестве параметра операции, она будет (при необходимости) приведена к нужному типу. Например, если для параметра типа int* указать специализированный тип со значением “1” и основанным на int, то будет создан следующий код: 119
... int Par_1 = 1; someFunction(&Par_1); ... Такие действия можно предпринимать внутри классов эквивалентности типов, являющихся производными с помощью модификаторов const, * или &, а также с помощью операции typedef, от одного и того же типа. Данные любого типа можно привести к любому другому типу в рамках его класса эквивалентности. Специализированный тип может использоваться вместо любого типа из класса эквивалентности его исходного типа. Это можно интерпретировать как разновидность наследования: при несовпадении типа параметра и исходного типа данных специализированного типа, происходит создание нового специализированного типа, основанного на типе параметра и наследующего этот специализированный тип. В действительности создавать новый тип не нужно — он генерируется автоматически, но из-за схожести с процессом наследования данный механизм называется неявным наследованием.
4.3. Единожды используемые специализированные типы При расширении описания операции разработчику приходится выбирать нужный специализированный тип из списка созданных на основе типа данного параметра или результата операции. Часто этот список бывает настолько велик, что трудно найти необходимый тип. При этом в списке могут встречаться типы, каждый из которых используется только одной операцией, из-за того, что сильно привязан к ее специфике. С большой долей вероятности они больше никогда использоваться не будут. Поэтому их присутствие в списке возможных специализированных типов излишне и приводит к перегруженности этого списка. Такие специализированные типы называются единожды используемыми или одноразовыми. Это свойство указывается в атрибуте “Вид” этих типов. Как ясно из его названия, одноразовый специализированный тип можно использовать только один раз. Он не появляется в списке специализированных типов, построенных для некоторого исходного. Следует отметить, что точно определить будет ли использоваться где-нибудь еще создаваемый специализированный тип невозможно. Но, в крайнем случае, придется создать новый тип или изменить вид старого (если знать о его существовании). В качестве примера одноразового специализированного типа можно привести тип для возвращаемого значения метода className() для любого класса из библиотеки Qt3 [3], например для QPushButton. Данный метод должен вернуть название класса, т.е. строку “QPushButton”. В ограничении у соответствующего специализированного типа будет указано логическое 120
выражение: “QString($0) == "QPushButton"”. Исходный тип данных — char*. В данном случае и в случаях, подобных этому, специализированный тип должен быть одноразовым. Иначе в списке допустимых типов для char будет большое количество типов, проверяющих названия классов, хотя они нигде больше использоваться не будут.
4.4. Cпециализированные типы «по умолчанию» Если в расширенном описании для параметра не указан специализированный тип, то работа по инициализации параметра полностью лежит на генераторе — программе, которая по расширенному описанию операции создает для нее тест. В зависимости от ситуации генератор может проинициализировать параметр конкретным значением (для простых типов), конструктором или вызовом другого интерфейса с подходящим возвращаемым значением. Но иногда такая инициализация без использования специализированных типов приводит к нежелательному результату. В этом случае нужно создать (или выбрать из уже созданных) подходящий специализированный тип. Как правило, для классов существует выделенный специализированный тип, который позволяет создать объект этого класса и задать для него некоторые общие свойства. Такой тип используется разработчиком как специализированный тип “по умолчанию”. Т.е. всегда, когда не нужно инициализировать объект каким-то особым образом, для него устанавливается этот специализированный тип. В таком случае удобно указать генератору, чтобы он при отсутствии у параметра специализированного типа считал, что его нужно проинициализировать с помощью именно этого специализированного типа. Для этого, нужный тип следует задать как тип по умолчанию для параметров. Это свойство указывается в атрибуте “Вид” соответствующего специализированного типа. Аналогичная ситуация и для специализированных типов, проверяющих возвращаемые значения. Такой тип можно определить как тип по умолчанию для возвращаемых значений некоторого исходного типа. В случае если на возвращаемое значение какой-либо операции не наложено никаких ограничений, но существует тип по умолчанию для возвращаемых значений такого типа, генератор воспользуется именно им для проверки правильности возвращенного результата. Обычно такие специализированные типы проверяют общие свойства, например: “$0.isValid() == true”, “$0.isEmpty == false”, “$0.isNull() == false” и т.д. Такие проверки вынуждают подбирать такие параметры вызова операции, чтобы эти условия были выполнены, или указывать обычные специализированные типы, проверяющие обратные условия (“$0.isValid() == false”, “$0.isEmpty == true”, “$0.isNull() == true” и т.д). В целом специализированные типы “по умолчанию” позволяют с одной стороны снизить усилия на уточнение описания операции, а с другой стороны более аккуратно нацелить тесты на нормальные сценарии использования 121
операций с помощью задания корректных путей инициализации объектов “по умолчанию” и автоматической вставки проверок возвращаемых значений.
4.5. Методы именования специализированных типов Чтобы среди списка специализированных типов было удобно искать необходимый в данный момент, нужно следовать некоторым правилам при выборе названий этих типов. Иначе по названию сложно понять функции конкретного специализированного типа, и вместо повторного использования одного из имеющихся типов в большинстве ситуаций создаются новые. В рамках нашей работы были приняты следующие принципы построения названий специализированных типов. Содержательность. Из названия должно быть ясно, чему равно значение, что проверяется в ограничении или какой метод объекта вызывается в инициализирующем коде. Краткость. Название специализированного типа не должно содержать избыточной информации. Примером такой информации может служить название исходного типа данных. Кроме того, в названии не должны быть отражены несущественные свойства уточняющего типа (базовый тип, вид и т.д.). В зависимости от специфики исходного типа существует несколько правил для названия специализированных типов. Для встроенных типов (int, bool, float, char* и т.д.), как правило, существует много специализированных, которые непосредственно задают значение для соответствующих параметров. Поэтому названия этих специализированных типов совпадают с их единственным значением. Например: 1, –10, “Test String”, true.
122
Названия специализированных типов для перечислений строятся аналогично. Единственное отличие в том, что в название нужно включать пространства имен, т.к. оно не всегда совпадает с именем исходного типа соответствующего специализированного типа. Например: Qt::AlignHCenter, QGL::SingleBuffer, QTextEdit::AutoAll.
Для классов специализированных типов, как правило, не много, но большинство содержат вспомогательный код, смысл которого может быть сразу неясен. Поэтому названия таких специализированных типов должны его раскрывать. В этом случае оно состоит из слова обозначающего действие, направленное на этот объект (Create, Fill, Call и т.д.), и некоторой информации, поясняющей это действие. Например: Create, Create_Simple, Create_qRgb(15, 30, 200), Create_Filled_With_3_elements, Call_begin().
Если в специализированном типе есть проверка на возвращаемое значение, то его название начинается с “R_” (что является сокращением от “Return”). После “R_” следует проверяемое ограничение или значение, равенство которому проверяется. В случае необходимости до “R_” указываются исходные данные. Пример: R_Null, R_33, R_NotEmpty, true_R_true (значит, подали true и вернуться должно true). Parameter
Interface
Type
LSB TGParameter
TGInterface
TGPint
TGIid
TGPpos
TGIspecreturn
TGPproxy
TGIspecobjecttype TGIreadyfortest
TGParameterProxy TGPPid TGPPspectype TGPPvalue TGSpecTypeValue TGSTVid TGSTVspectype TGSTVvalue Таблицы для специализированных типов
TGIcomment TGIkind TGItestresult
TGType TGTid TGTbasetype
5.1. Необходимые таблицы из базы данных LSB
TGTlevel
База данных LSB включает в себя большое число таблиц (примерно 50), но для создания тестов важны только следующие три. Таблица Interface содержит информацию об операциях стандартизованных библиотек: название операции, тип возвращаемого значения (ссылка на таблицу Type), содержащая ее библиотека, заголовочный файл, в котором она декларирована. Кроме того, указывается является ли операция методом класса и какого, а также свойства этого метода: конструктор ли он или деструктор, его доступность (public, private, protected) и контекст (static или не-static). Таблица Parameter содержит информацию о параметрах каждой операции: тип параметра (ссылка на таблицу Type) и номер параметра в сигнатуре операции. Таблица Type содержит информацию о типах: название, вид (intrinsic, struct, class, typedef, union и т.д.), заголовочный файл, в котором он определен. Эта таблица связана с таблицами Interface и Parameter.
TGTarch TGTpriority TGInterfaceSupplement TGISint TGISpreamble
TGSpecType TGSTid TGSTname TGSTcomment TGSTtype TGSTbasetype TGSTcode TGSTkind
необходимую для автоматического создания тестов. На Рис. 2 приведена схема этого расширения. Расширение базы данных LSB включает в себя 8 таблиц, название которых начинается на TG (что значит Test Generation). Из них 4 таблицы хранят информацию о специализированных типах: TGSpecType, TGSpecTypeValue, TGSpecTypeConstraint, TGParameterProxy (на схеме эти таблицы выделены жирным пунктиром). Таблицы TGParameter и TGInterface служат для связи таблиц уточняющих типов с таблицами Parameter и Interface из LSB, TGType хранит информацию об исходных базовых типах для каждого типа данных, а TGInterfaceSupplement хранит код, создающий корректное окружение для вызова соответствующего интерфейса (операции). Более подробно эти таблицы описаны в разделе 5.3.
TGISfinalization TGISinmain TGSpecTypeConstraint TGSTCid TGSTCspectype TGSTCcode TGSTCkind
5.2. Таблицы для специализированных типов
Рис. 2. Расширение базы данных LSB
5. Расширение базы данных LSB Описанный в данной работе механизм специализированных типов был реализован в рамках проекта Linux Foundation в ИСП РАН. За основу была взята база данных LSB, содержащая необходимое описание сигнатур операций и типов данных. Она была расширена таблицами, содержащими специализированные типы и некоторую вспомогательную информацию, 123
Специализированные типы хранятся в 4 таблицах: 1. TGSpecType — основная таблица. Она хранит основные свойства специализированного типа: TGSTid — содержит уникальный идентификатор специализированного типа. По этому полю происходит связь других таблиц с таблицей TGSpecType. TGSTname — название специализированного типа. TGSTtype — исходный тип данных (ссылка на таблицу Type). TGSTbasetype — содержит идентификатор “родителя” для данного специализированного типа (ссылку на таблицу TGSpecType). C помощью этого поля реализуется механизм наследования специализированных типов. 124
TGSTkind — вид специализированного типа: normal (обычный), once-only (используемый один раз), common for parameter (“по умолчанию” для параметров), common for return (“по умолчанию” для возвращаемых значений). TGSTcode — вспомогательный код. TGSTcomment — содержит текстовое описание типа.
2.
TGSpecTypeValue — таблица для значений специализированных типов. Главным полем является TGSTVvalue, содержащее значение для специализированного типа. Для одного специализированного типа может существовать несколько значений.
3.
TGSpecTypeConstraint — таблица для ограничений специализированного типа. Поле TGSTCcode содержит само ограничение, а поле TGSTCkind — его вид: “Normal Result”, “Init Code” — инициализирующий код, “Final Code” — финализирующий код.
4.
TGParameterProxy — служит для связи таблицы специализированных типов TGSpecType с таблицей параметров TGParameter. C таблицей TGSpecType она связана через поле TGPPspectype, которое хранит идентификатор специализированного типа, а с таблицей TGParameter — через поле TGPPid, на которое ссылается поле TGPproxy из таблицы TGParameter. При создании связи с параметром для обычного специализированного типа, таблица TGParameterProxy не играет никакой роли: для одного типа создается одна запись в этой таблице, на которую указывает поле TGPproxy из таблицы TGParameter. Но ее роль становиться ключевой в процессе создания комплексного типа. Если он создается для нескольких параметров, то в таблице TGParameterProxy появляется несколько записей, соответствующих одному специализированному типу (имеющих одинаковые значения в полях TGPPspectype). Причем каждая запись в поле TGPPvalue содержит прокси-значения, которыми будут проинициализированы соответствующие параметры.
Поле TGIkind определяет можно ли для текущей операции создавать тест. Его значения: o Suitable for test — годна для тестирования o No data — не хватает данных в базе данных для создания теста o Not documented — операция не имеет документации o Complex test — нельзя создать примитивный тест. Поле TGIcomment хранит текстовое описание проблем, из-за которых создать тест для данного интерфейса не удалось.
TGType — для каждого типа данных содержит базовый тип (TGTbasetype), от которого данный является производным с помощью модификаторов const, *, &, количество модификаторов *, примененных при определении данного типа (TGTlevel), зависимость от архитектуры (TGTarch — некоторые типы на разных архитектурах являются производными от различных типов) и приоритет (TGTpriority). Приоритет важен для классов, операции которых тестируются. Он определяет порядок уточнения сигнатур операций классов. Приоритет класса, в качестве типов параметров операций которого используются другие классы, должен быть не выше приоритета этих классов. Чем выше приоритет, тем раньше нужно обработать соответствующий класс.
TGInterfaceSupplement — содержит вспомогательную информацию для создания теста для некоторой операции.
5.3. Дополнительные таблицы Кроме таблиц, хранящих данные для специализированных типов, в расширении базы данных LSB есть еще 4 таблицы. TGParameter — служит для связи таблицы TGParameterProxy с таблицей Parameter. Через поля TGPint (идентификатор интерфейса) и TGPpos (номер параметра в интерфейсе) она связана с таблицей Parameter. Поле TGPproxy хранит ссылку на таблицу TGParameterProxy. Фактически, чтобы задать для параметра специализированный тип, нужно указать в поле TGPproxy ссылку на прокси значение соответствующего типа. 125
TGInterface — через поле TGIid связано с таблицей Interface. Поле TGIspecreturn и TGIspecobjecttype содержат ссылки на специализированные типы для возвращаемого значения и объекта соответственно. Остальные поля используются в процессе создания тестов. Поле TGIreadyfortest указывает готовность данной операции к тестированию. Поле TGItestresult содержит результат последнего запуска теста для данной операции. Результат может принимать следующие значения: o Unknown — неизвестен o Success — тест создался, откомпилировался и запустился без ошибок o Generation failed — произошла ошибка во время генерации теста o Compilation failed — произошла ошибка во время компиляции теста o Execution falied — произошла ошибка во время работы теста o Requirenment failed — было нарушено условие на возвращаемое значение
126
o o o
TGISpreamble — содержит код, который вставляется до вызова целевой операции. TGISfinalization — содержит код, который вставляется после вызова целевой операции. TGISinmain — указывает, нужно ли вызывать целевой интерфейс в функции main() или в отдельной, специально созданной функции (данное поле важно при тестировании некоторых библиотек, например Qt [3,4]).
6. Заключение В данной работе была рассмотрена задача определения и представления информации, необходимой дополнительно к сигнатурам операций для автоматической генерации простых тестов, вызывающих эти операции с корректными параметрами и в корректном окружении и проверяющих отсутствие грубых ошибок исполнения. Эту информацию предложено представлять в виде так называемых специализированных типов, которые уточняют семантику параметров и возвращаемых значений тестируемых операций, позволяя автоматически строить инициализацию данных и окружения, необходимых для вызова этих операций. Были предложены также различные механизмы, позволяющие уменьшать количество создаваемых специализированных типов в рамках работы с группой связанных функций, что существенно снижает трудоемкость задания расширенной информации для таких групп. Показана реализация хранения расширенной информации о специализированных типах и их привязка к данным об операциях на примере расширения базы данных стандарта LSB, реализованные в проекте LSB Infrastructure [5]. На основе этой реализации была задана расширенная информация для почти 10000 операций библиотеки Qt3 [3]. При этом было создано около 1600 специализированных типов, а производительность создания тестов, с учетом их отладки, составила в среднем 70 операций в день на человека. Литература [1] [2] [3] [4] [5]
http://www.linuxbase.org. http://www.linux-foundation.org/navigator/commons/welcome.php. http://doc.trolltech.com/3.3/index.html. http://doc.trolltech.com/4.2/index.html. http://ispras.linux-foundation.org/.
127
Целью алгоритма генерации является построение тестов работоспособности на основе имеющихся исходных данных. Специализированный тип для QString
Алгоритм генерации тестов работоспособности на основе расширенной базы данных LSB
Специализированный тип для QWidget*
QString QWidget* QString&
А. В. Пономаренко, Е. С. Чернов {susanin, ches}@ispras.ru
QWidget
Специализированный тип для Qt::WFlags
Аннотация. В статье рассматривается алгоритм автоматической генерации тестов на основе базы данных LSB, содержащей описания сигнатур функций и системы типов, и ее расширения, представляющего собой описание некоторых ограничений стандарта для этих функций и служащее для корректной инициализации параметров и проверки ряда требований к корректности результатов.
Конструктор класса
Конструктор класса QWidget
Qt::WFlags
const char*
1. Введение Стандарт Linux Standard Base (LSB) включает описание более 30000 функций, число которых растет с каждой его новой версией. Для проверки реализаций функций на соответствие стандарту LSB необходимо создать для них тесты. В настоящее время существуют тесты примерно для 5000 функций, поэтому задача создания тестов для остальных функций является актуальной, но создание качественных тестов для них требует больших затрат. В этой ситуации привлекательной выглядит идея создания на первом этапе тестов работоспособности для всех непротестированных функций, проверяющих работу этих функций только в простейших ситуациях, а на втором этапе — создание тестов, детально проверяющих корректность работы наиболее значимых из них [1]. Задачу первого этапа — массового создания тестов работоспособности — решает технология Azov. Высокая производительность создания тестов достигается в ней за счет высокой степени автоматизации, поэтому существенную роль играют алгоритм генерации тестов. В качестве исходных данных алгоритм генерации используют базу данных LSB, содержащую сигнатуры функций и описание системы типов данных, и ее специальное расширение, содержащее описания уточнений входных и выходных данных функций. Эти уточнения являются формализацией части требований документации на функции (например, стандарта) и представлены в виде абстрактных конструкций — специализированных типов [2]. 129
char*
char
Специализированный тип для const char*
Рис. 1. Граф зависимостей исходных данных для конструктора класса QLabel В первом разделе статьи рассматриваются основные принципы алгоритма генерации тестов. Во втором и третьем разделе раскрываются шаги алгоритма. В четвертом разделе приведен пример теста работоспособности.
2. Основные принципы алгоритма генерации 2.1. Представление зависимостей
исходных
данных
в
виде
графа
Исходные данные можно разбить на две группы — описания сигнатур функций с исходными типами их параметров и результатов и описания специализированных типов для этих же параметров и результатов. Данные из первой группы можно представить в виде графа зависимостей. Для этого функции отождествляются со структурными типами — параметры функции суть поля структурного типа. В этом графе каждый узел — это некоторый тип данных (в том числе и функция). Узел графа указывает на другие узлы, если 130
Блок 1 “Информация о тесте и комментарии” — информация о том, когда и для какой аппаратной архитектуры сгенерирован тест, а также о сигнатуре целевой функции.
Блок 2 “Подключение заголовочных файлов” — содержит директивы на подключение заголовочных файлов вида “#include
” необходимых для теста.
Блок 3 “Определение вспомогательных функций” — взятые SpecCode определения непосредственно из атрибута вспомогательных функций.
Блок 4 “Определение классов-наследников” — содержит определение используемых в тесте классов-наследников некоторых классов (см. раздел 3.2).
2.2. Структура теста
Блоки 5 и 14 обозначающие начало и конец функции main().
Выбрана традиционная структура генерируемых тестов, состоящая из различных блоков (см. Рис. 2).
Блоки 6 “Программный код в начале теста” и 13 “Программный код в конце теста” — соответствуют специальным ограничениям на целевую функцию Preamble и Finalization.
Блок 7 “Инициализация параметров целевой функции” конструирование фактических параметров целевой функции.
Блоки 8 “Программный код перед вызовом целевой функции” и 11 “Программный код после вызова целевой функции” — соответствуют атрибутам специализированных типов InitCode и FinalCode.
он от них зависит. Например, тип зависит от своего базового типа (от которого данный тип является производным) и от функции возвращающей этот тип, функция зависит от узлов, отвечающих за типы ее параметров. На Рис. 1 изображен граф зависимостей исходных данных для конструктора класса QLabel, имеющего сигнатуру “QLabel::QLabel(QString&, QWidget*, const char*, Qt::WFlags)”. Второй параметр этого конструктора имеет тип QWidget*, базовым типом которого является класс QWidget. Объект этого класса можно получить с помощью конструктора “QWidget::QWidget(QWidget*, const char*, Qt::WFlags)”. Данные из второй группы — специализированные типы — дополняют граф зависимостей (на Рис. 1 специализированные типы изображены пунктиром). Зависимость в графе обычного типа от специализированного типа означает, что при построении экземпляра этого типа будут использоваться атрибуты соответствующего специализированного типа.
1
Информация о тесте и комментарии
2
Подключение заголовочных файлов
3
Определение вспомогательных функций (SpecCode)
4
Определение классов-наследников
5
int main () {...
Блок 9 “Вызов целевой функции”.
6
Программный код в начале теста (Preamble)
7
Конструирование параметров целевой функции
8
Программный код перед вызовом целевой функции (InitCode)
Блок 10 “Проверка некоторых требований” — код, полученный из атрибута NormalResult специализированных типов и сигнализирующий об ошибке в случае нарушения требований наложенных на целевую функцию.
9
Вызов целевой функции
10
Проверка некоторых требований (NormalResult)
Блок 12 “Освобождение памяти” — содержит вызовы деструкторов для всех созданных объектов в тесте.
11
Программный код после вызова целевой функции (FinalCode)
12
Освобождение памяти
13
Программный код в конце теста (Finalization)
14
... return 0; }
—
Инициализацию фактических параметров целевой функции (блок 7) можно представить в виде цепочки вызовов других функций. Одной из основных задач алгоритма генерации является построение таких цепочек инициализации.
2.3. Представление цепочки функции в виде дерева
инициализации
параметров
Цепочку инициализации фактических параметров целевой функции можно представить в виде дерева вывода графа зависимостей исходных данных для этой функции. Корень дерева — это узел, содержащий целевую функцию.
Рис. 2. Структура теста работоспособности 131
132
Узлы дерева — типы данных (или функции), используемые при инициализации параметров целевой функции. Листья дерева содержат некоторое значение, с помощью которого инициализируется соответствующий тип данных. Дерево вывода для конструктора “QLabel::QLabel(QString&, QWidget*, const char*, Qt::WFlags)” изображено на Рис. 3. Первый параметр этого конструктора инициализируется с помощью конструктора “QString::QString(const char*)”, второй — с помощью конструктора “QWidget::QWidget(QWidget*, const char*, Qt::WFlags)”. Третий и четвертый параметры инициализируются соответственно произвольной строкой и одним из возможных значений перечисления Qt::WFlags.
2.4. Шаги алгоритма генерации тестов Генерация тестов основана на выполнении двух шагов: построении дерева вывода в графе зависимостей исходных данных и генерации кода теста на основе этого дерева. Далее в разделе 3 описана последовательность построения дерева вывода, а в разделе 4 — последовательность генерации программного кода на основе построенного дерева вывода.
3. Построение дерева вывода 3.1. Основные характеристики типов данных Дерево вывода строится на основе графа зависимостей исходных данных, узлами которого являются различные типы и функции. Основными характеристиками любого типа данных Type в графе зависимостей с точки зрения построения дерева вывода являются следующие. Порождающий тип (originative type) — тип данных, который не зависит ни от какого другого типа в графе зависимостей исходных данных, при этом Type является производным от него. Порождающий тип не построен ни из какого другого при помощи модификаторов *, &, const или определения синонима с помощью typedef. Например, порождающим типом для типа “QThread**” является “QThread”, а для типа “const char*” — тип “char”.
Базовый тип (base type) — тип данных, от которого непосредственно зависит Type в графе зависимостей исходных данных. Type строится из базового с помощью одного из модификаторов *, &, const или с помощью typedef.
Уровень вложенности указателей (pointer level). Это количество модификаторов *, использованных при построении данного типа из порождающего. Уровень вложенности указателей типа “QThread**” равен двум, а для типа “const char*” — равен единице.
Множество родственных типов (type family) — определяется, как множество типов имеющих один и тот же порождающий тип. Например, для типа Display* множеством родственных типов является {_XDisplay, _XDisplay*, Display, Display*, Display**, Display***}, так как порождающим типом у всех является тип _XDisplay.
Конструктор класса QString&
Qt::WFlags
QString
Конструктор класса
char*
const h *
QWidget*
Конструктор класса QWidget
Qt::WStyle_Splash
“Str3”
3.2. Расширение графа зависимостей исходных данных виртуальными специализированными типами
QWidget*
Qt::WFlags const char*
“Str1”
NULL
“Str2”
Qt::WType_Desktop
Рис. 3. Пример дерева вывода для конструктора класса QLabel
133
Перед построением дерева вывода граф зависимостей исходных данных дополняется особым видом специализированных типов — виртуальными специализированными типами. Такие специализированные типы генерируются автоматически. Специализированные типы содержат следующие атрибуты: InitCode — программный код, помещаемый перед вызовом функции; FinalCode — программный код, помещаемый после вызова функции; NormalResult — логическое выражение для проверки правильности результата функции; 134
SpecValue — значение, с помощью которого инициализируется параметр; SpecCode — программный код, содержащий определения функций используемых в других атрибутах; DataType — тип данных, который уточняется специализированным типом.
Различают два вида виртуальных специализированных типов: для параметров и для возвращаемых значений.
3.2.1. Виртуальные параметров
специализированные
типы
для
Этот вид специализированных типов содержит атрибуты SpecValue и SpecCode, тем самым, достраивая до конца дерево инициализации параметра. В зависимости от порождающего типа для разных множеств родственных типов виртуальные специализированные типы создаются следующим образом. Встроенные типы (intrinsic) — значения SpecValue для типа int задаются последовательно из множества {1, 2, 3 …}, для char — из множества {‘A’, ‘B’, ‘C’ …}, для double — из множества {1.0, 2.0, 3.0 …}, для bool — из множества {true, false} и т.д. Перечисления (enum) — значения SpecValue задаются некоторым значением из списка возможных значений данного перечисления. Любой тип (intrinsic, struct, union, class) с уровнем указателя большим единицы, для которого нет других возможностей инициализации — значение SpecValue этого специализированного типа выбирается равным NULL. Классы (class) — атрибут SpecCode содержит определение классанаследника, значение SpecValue содержит вызов одного из конструкторов этого класса-наследника. В классе-наследнике переопределяются автоматически чисто виртуальные методы, определяются необходимые конструкторы и деструкторы, а так же методы, напрямую вызывающие protected-методы исходного класса. В последнем случае виртуальные специализированные типы создаются, если необходимо вызвать protected-метод для объекта или сконструировать объект абстрактного класса.
3.2.2. Виртуальные специализированные возвращаемых значений
типы
для
Если возвращаемое значение функции имеет тип с уровнем указателя большим единицы, то генерируется виртуальный специализированный тип атрибутом NormalResult содержащим проверку на неравенство NULL. 135
3.3. Выбор направления построения дерева вывода в узле графа зависимостей исходных данных Узлы графа зависимостей исходных данных могут быть трех видов: типы, функции и специализированные типы. Поскольку узлы, отвечающие за типы данных, могут зависеть от нескольких других узлов, то построение дерева вывода может быть продолжено в направлении любого из них. Поддерживаются следующие политики выбора направления построения дерева в узле: конструирование, вызов функции и присваивание явно указанного значения.
3.3.1. Конструирование Конструирование осуществляется разными способами в зависимости от типа узла: Указатель на тип (pointer) — конструирование базового типа с последующим приведением к типу параметра. Структуры (struct) и объединения (union) — инициализация полей. Массивы (array) — инициализация элементов массива. Указатель на функцию (func-ptr) — определяется простейшая функция с нужными параметрами и проинициализированным возвращаемым значением. Классы (class) — вызов конструктора.
3.3.2. Вызов функции По графу зависимостей исходных данных для данного типа определяется множество функций с возвращаемым значением этого же типа. Это множество сортируется по разным характеристикам: Уровень доступа (public, protected) — проще вызывать publicфункцию, чем protected-функцию. Происхождение (библиотека, класс) — лучше вызывать функцию из тех же библиотеки или класса, в которых определена целевая функция. Число параметров — проще вызывать функции с меньшим числом параметров. Затем последовательно для каждой найденной функции строится дерево вывода соответствующего графа зависимостей исходных данных. Поиск заканчивается, когда будет найдена функция, для которой это дерево удалось успешно построить.
3.3.3. Присваивание значения Если рассматриваемый узел графа зависит от какого-нибудь специализированного типа (или виртуального специализированного типа) 136
имеющего значение атрибута SpecValue, то построение дерева вывода будет осуществляться в направлении этого специализированного типа.
3.4. Поиск корректного дерева зависимостей исходных данных
вывода
в
графе
Поиск дерева вывода производится рекурсивно. Начальным узлом служит узел, соответствующий целевой функции. На каждом шаге рекурсии для текущего узла графа зависимостей определяется направление дальнейшего построения дерева вывода, которое порождает множество узлов графа для следующего шага рекурсии. Для избегания бесконечной рекурсии, возникающей из-за существующих циклов в графе зависимостей необходимо помечать использованные узлы графа и далее при анализе графа их не использовать. Корректный выход из рекурсии осуществляется по достижении листьев, для которых известно конкретное значение, с помощью которого они могут быть проинициализированы. Если в некоторой ветви рекурсии не удается проинициализировать тип, то она считается некорректной и происходит выход из этой ветви с ошибкой. Это приводит к выбору другого пути в графе зависимостей на более высоком уровне рекурсии и, в целом, к перебору возможных путей в нем. Во время поиска пути инициализации могут встретиться специализированные типы. В этих случаях путь инициализации фиксирован и определяется соответствующим специализированным типом.
4. Генерация программы на основе дерева вывода 4.1. Приведение родственных типов данных друг к другу При генерации программного кода на основе ребра, связывающего узлы, отвечающие за родственные типы данных, используется приведение типов. Приведение одного типа к другому родственному типу осуществляется на основе уровней вложенности указателей этих типов. Приведение типа Type1 с меньшим уровнем указателя к типу Type2 с большим уровнем указателя осуществляется посредством создания массива из элементов типа Type1, затем массива массивов из элементов Type1 и так далее до получения нужного уровня вложенности указателей типа Type2.
Приведение типа Type1 с большим уровнем указателя к типу Type2 с меньшим уровнем указателя осуществляется посредством последовательного разыменования переменной типа Type1 до получения нужного уровня вложенности указателей типа Type2.
137
4.2. Получение экземпляра типа на основе вызова функции Программный код на основе ребра, ведущего от некоторого типа к функции, включает вызов этой функции и присвоение ее возвращаемого значения экземпляру соответствующего типа.
4.3. Генерация программного кода на основе специализированного типа и специальных ограничений для функции При генерации программного кода на основе ребра, связывающего любой узел дерева вывода с узлом, отвечающим за специализированный тип необходимо правильно интерпретировать атрибуты этого специализированного типа. Сначала в атрибутах InitCode, FinalCode, SpecValue и NormalResult специализированного типа осуществляется замена специальных конструкций: $0 — идентификатор целевого параметра. Вместо него подставляется экземпляр специализированного типа; $obj — идентификатор объекта. Вместо него подставляется экземпляр класса целевого метода. Данные конструкции позволяют описать в специализированном типе действия над параметром, независимо от имени переменной этого параметра. Содержимое атрибутов InitCode, FinalCode располагается до и после вызова функции имеющей параметр соответствующего специализированного типа. Значение SpecValue присваивается экземпляру специализированного типа. Значение атрибута NormalResult представляет собой логическое выражение, которое должно быть истинным в случае правильной работы функции. Из него формируется код, проверяющий правильность результата функции и сигнализирующий об ошибке в случае нарушения требования: if (Not NormalResult) { //Сообщение об ошибке } В атрибуте SpecCode специализированного типа содержится определение вспомогательных функций, которые используются в других атрибутах этого специализированного типа. В их телах иногда требуется инициализировать вспомогательные данные, для чего могут быть использованы следующие конструкции: $(имя типа) — указание проинициализировать и подставить переменную указанного типа: $[имя функции (или метода)] — указание проинициализировать параметры и подставить вызов указанной функции (или метода)
138
Рассмотрим некоторый специализированный тип, который служит для инициализации параметра типа QListBoxItem — элемента в списке QListBox. Примером атрибута SpecCode может послужить следующий код, содержащий определение вспомогательной функции create_QListBoxItem(), инициализирующей тип QListBoxItem посредством вызова метода QListBox::firstItem для которого уже внесены необходимые специализированные типы:
файла) и относящиеся ко всем функциям некоторой системы (создание и запуск приложения в библиотеке Qt).
5. Пример теста работоспособности
Специализированный тип для объекта метода QListBox::firstItem указывает провести инициализацию параметра с помощью вызова вспомогательной функции create_QListBox для получения непустого списка типа QListBox. Ниже приведена часть программы, полученная с помощью этого специализированного типа:
Рис. 4. Пример теста работоспособности для метода QMutex::locked Специализированные типы расширяют описания параметров функций и возвращаемых значений. Кроме этого есть ограничения, привязанные непосредственно к самим функциям — необходимый код в самом начале и конце теста (соответственно преамбула (Preamble) и постамбула (Finalization)). Преамбула и постамбула бывают двух типов — непосредственно относящиеся к конкретной функции (например, инициализация необходимого окружения или удаление созданного функцией 139
Пример теста работоспособности для метода QMutex::locked() из библиотеки Qt3 изображен на Рис. 4. Объект класса QMutex для вызова этого метода создается автоматически при помощи вызова конструктора QMutex::QMutex(bool). Для класса QMutex задан специализированный тип, содержащий атрибуты InitCode, NormalResult, FinalCode, включающие соответственно блокировку (вызов метода lock()), проверку блокировки (результат вызова метода locked() должен быть равен true) и разблокировку 140
(вызов метода unlock()) экземпляра этого класса. Специальные ограничения на функцию Preamble и Finalization содержат создание и запуск приложения (приложение в библиотеке Qt3 — объект класса QApplication). В целом этот тест вызывает целевой метод с корректными параметрами и проверяет правильность его работы.
6. Заключение Рассмотренный алгоритм генерации тестов работоспособности позволяет автоматически в течение нескольких часов получать десятки тысяч тестов. Таким образом, получена возможность быстрого создания тестов для любых уже имеющихся и новых библиотек в LSB. Дальнейшее улучшение алгоритма генерации тестов наряду с разработкой структуры расширений базы данных LSB приведет к повышению степени автоматизации создания тестов и повышению их качества. Литература [1] Р. Зыбин, В. Кулямин, А. Пономаренко, В. Рубанов, Е. Чернов. Технология Azov автоматизации массового создания тестов работоспособности. Опубликовано в этом же сборнике. [2] Р. Зыбин, А. Пономаренко, В. Рубанов, Е. Чернов. Расширение описаний сигнатур операций для автоматической генерации тестов. Опубликовано в этом же сборнике. [3] http://www.linuxbase.org. [4] http://www.linux-foundation.org/en/LSB.
141
Опыт применения технологии Azov для тестирования библиотеки Qt3 Р. С. Зыбин [email protected] Аннотация. В данной статье рассматривается задача адаптации технологии Azov для построения тестового набора, проверяющего работоспособность интерфейса библиотеки Qt3 для разработки приложений с графическим пользовательским интерфейсом. Приводится уточнение базовой методики, которое позволяет формировать корректные тесты с учетом специфики языка C++ и дополнительных возможностей тестируемой библиотеки. Вводятся расширения технологии, позволяющие ускорить работу над созданием тестового набора. Полученная методика показывает высокую эффективность при разработке простейших тестов работоспособности для сложных интерфейсов, содержащих большое количество методов и функций. Отдельно обсуждаются достоинства и недостатки технологии, выявленные в процессе ее реализации, а также указываются возможные направления ее дальнейшего развития.
1. Введение Тестирование работы программы в различных ситуациях остается одним из самых широко используемых способов демонстрации ее корректности, особенно для достаточно сложного приложения. Но все существующие методики создания тестов, обеспечивающие их полноту в соответствии с некоторыми естественными критериями, требуют весьма значительных трудозатрат. Однако в ряде случаев, например, при тестировании же объемных интерфейсов, содержащих тысячи операций, полное и тщательное тестирование обходится слишком дорого, а иногда и вовсе не требуется для всех элементов интерфейса системы. Вместо этого проводится тестирование работоспособности, то есть проверяется, что все функции системы устойчиво работают хотя бы на простейших сценариях использования. Уже затем, для наиболее критической части интерфейса разрабатываются детальные тесты. Именно для решения таких типов задач предназначена технология Azov [1], созданная в 2007 году в Институте системного программирования. Технология нацелена на разработку тестов работоспособности, вызывающих тестируемые операции в правильном окружении с какими-нибудь 143
допустимыми значениями параметров, характеризующими простейшие сценарии использования этих операций. При этом она позволяет существенно автоматизировать создание теста. Область применимости технологии включает те случаи, когда информация об интерфейсе доступна в хорошо структурированном, годном для автоматической обработки виде. При этом трудозатраты на создание одного теста существенно меньше, если тестируемый интерфейс содержит большое количество операций. В частности технология Azov была использована при создании тестового набора для бинарных операций библиотеки Qt3, предназначенной для разработки приложений с графическим интерфейсом [4] и входящей в стандарт Linux Standard Base [2,3] (LSB). Стандарт включает в себя около 10000 методов и функций библиотеки и хранит информацию об их синтаксисе в своей базе данных. Тестирование библиотек, написанных на языке C++, в частности Qt3 [4], является достаточно трудоемкой задачей. Поведение метода может зависеть не только от передаваемых ему параметров и глобального состояния системы, но и от состояния объекта, которому он принадлежит. Сама же библиотека графического интерфейса содержит дополнительные механизмы, которые также оказывают существенное влияние на работу методов. Поэтому базовая методика, предлагаемая технологией Azov, должна быть дополнена набором средств, позволяющих формировать работоспособные тесты на языке C++ во всевозможных ситуациях, возникающих при тестировании элементов интерфейса, и учитывающих особенности библиотеки Qt [3], в частности механизм передачи сообщений.
2. Методика построения корректных тестов Основной задачей рассматриваемой технологии является построение корректного теста работоспособности для каждой операции, входящей в тестируемый интерфейс. Под корректным понимается такой тест, который обеспечивает правильную последовательность инициализации параметров целевой операции, инициализацию параметров среды исполнения, содержит непосредственный вызов целевой операции, а также освобождает по завершении задействованные ресурсы и возвращает систему в исходное состояние. Следует также отметить, что в тесте должно проверятся, выполнился ли вызов тестируемой операции успешно или наблюдается отклонение от ожидаемого поведения. Вопросы, касающиеся инициализации и финализации (освобождение захваченных тестом ресурсов) тестовых данных относятся скорее к технологии в целом, и освещены в статье [1]. Здесь же речь пойдет об 144
особенностях реализации общего подхода технологии Azov при использовании языка C++ и для тестирования библиотеки Qt3. Описанные ниже особенности обеспечиваются функциональностью инструмента-компоновщика тестов, генерирующего тесты по базе данных с уточненной информацией об интерфейсных операциях [1,5], и разработчику, как правило, не приходится о них беспокоиться.
2.1. Конструкторы и деструкторы В языке C++ каждому конструктору на программном уровне соответствуют два различных конструктора на бинарном уровне: in-charge и not-in-charge, а каждому деструктору – два или три бинарных: in-charge, not-in-charge и, дополнительно для виртуальных классов, – in-charge deleting. Поскольку стандарт LSB описывает поведение системы на бинарном уровне, то тестированию подлежат все вышеперечисленные конструкторы и деструкторы. In-charge конструкторы и деструкторы вызываются при непосредственной работе с объектом класса. In-charge deleting деструктор используется при удалении объекта виртуального класса из общей памяти. Таким образом, для конструктора и деструктора невиртуального класса компоновщик составляет тест, создающий объект либо в стековой, либо в общей памяти, а затем удаляющий его. В случае виртуального класса область памяти, в которой будет создаваться объект, зависит от тестируемого деструктора. Not-in-charge конструкторы и деструкторы используются неявно при работе с объектом какого-либо класса, наследующего данному. Поэтому в тесте описывается наследник, и путем создания и уничтожения объекта полученного типа опосредованно выполняется вызов этих конструкторов и деструкторов.
2.2. Абстрактные классы При тестировании ряда методов возникает необходимость построения объекта абстрактного класса. Следует, однако, уточнить, что непосредственно такой объект необходим только в том случае, когда тестируется его метод. В остальных же случаях можно, как правило, обойтись каким-либо объектом наследующего класса. В первом случае необходимо провести дополнительную работу по определению чисто виртуальных методов. Реализация функциональности каждого метода является трудоемкой задачей, решения которой при проведении тестирования работоспособности хотелось бы избежать. Поэтому компоновщик тестов автоматически генерирует код, в котором определяется класс-наследник абстрактного класса, имеющий те же методы, что и родитель. Чисто виртуальные методы реализованы как заглушки, возвращающие какоелибо значение необходимого типа данных. Это значение получается по общим правилам генерации тестов, то есть может быть либо взято из 145
предопределенного пользователем множества, либо сконструировано компоновщиком. Впрочем, в ряде случаев такой подход оказывается неприемлемым. Например, если заглушка используется каким-либо другим методом, вызываемым в процессе тестирования. В подобных ситуациях, если это явилось причиной падения теста, необходимо вручную реализовывать чисто виртуальные методы, основываясь, как правило, на исходных кодах одного из существующих наследников тестируемого класса. Дополнительно, стоит отметить, что язык C++ допускает реализацию по умолчанию абстрактного метода, которая может быть использована при описании наследника. Однако в библиотеке Qt3 такая возможность не используется. В тех же случаях, когда объект абстрактного класса выступает в качестве параметра, возможны различные варианты его инициализации. Во-первых, можно воспользоваться описанным выше способом и позволить компоновщику сконструировать объект. Во-вторых, можно доопределить абстрактный класс вручную. Однако более предпочтительным является использование объекта какого-либо класса, наследующего данному. Также могут встретиться методы, возвращающие указатель на нужный абстрактный класс. Разумеется, в действительности они возвращают указатель на объект наследующего класса. Результат работы таких методов также можно использовать в качестве значения параметра. Аналогично, при тестировании невиртуальных методов можно не доопределять абстрактный класс, а воспользоваться экземпляром одного из наследников тестируемого класса. In-charge конструкторы и деструкторы абстрактного класса протестировать невозможно, поскольку они предназначены для построения или разрушения объекта именно этого класса.
2.3. Защищенные методы Использование защищенных методов также требует некоторых дополнительных построений. Область видимости таких методов ограничена методами класса, которому принадлежит защищенный метод, или его наследников. При построении теста компоновщик генерирует описание класса, наследующего классу, содержащему защищенный метод. В нем определяется дополнительный общедоступный метод, являющийся оберткой защищенного. Параметры обоих методов совпадают, так что везде в тесте можно обращаться к построенному методу сгенерированного класса, подразумевая вызов требуемого защищенного метода. Из этого правила имеется исключение, а именно случай, когда метод имеет в качестве одного из своих параметров объект защищенного типа данных. В рамках Qt, например, встречаются защищенные перечисления (protected 146
enum). В такой ситуации цепочка инициализации объекта нужного типа генерируется компоновщиком в теле метода-обертки, а сам этот общедоступный метод имеет на один параметр меньше. Следует также добавить, что при таком подходе защищенные in-charge конструкторы и деструкторы не могут быть протестированы, поскольку они предназначены для работы с объектом исходного класса, а вызвать их можно только из объекта наследующего класса.
2.4. Методы, не приложения
входящие
в
программный
интерфейс
Определяя набор интерфейсных операций на бинарном уровне, стандарт LSB в некоторых случаях включает в себя операции, не являющиеся частью программного интерфейса (Application Programming Interface, API) описываемых им библиотек. Однако, при тестировании работоспособности необходимо, по возможности, протестировать даже подобные скрытые операции. В рамках Qt3 существует ряд классов, предназначенных исключительно для использования внутри самой этой библиотеки. Описания таких классов находятся либо в заголовочных файлах, предназначенных только для сборки библиотеки, либо и вовсе в исходных кодах. Одним из возможных подходов к тестированию методов таких классов является построение теста для метода, входящего в API, в процессе работы которого вызывается скрытый метод. Однако данный способ имеет большое количество недостатков. Во-первых, требуется весьма трудоемкое исследование того, какие методы могут быть использованы и какие значения их параметров приводят к вызову тестируемого метода. Во-вторых, отсутствует непосредственный контроль над параметрами тестового воздействия, а опосредованный контроль может быть слишком труден или даже вовсе невозможен. И, наконец, в-третьих, достаточно сложно определить, отработал ли тестируемый метод должным образом. Учитывая все сказанное, на практике используется существенно более простое решение. Найденное описание класса выносится в отдельный заголовочный файл, который поставляется вместе с тестом. Вообще говоря, вынесение описания в отдельный файл необходимо только для классов, использующих предоставляемые Qt расширения языка C++, поскольку прекомпилятор Meta Object Compiler, который генерирует на их основе определения на чистом C++, обрабатывает только файлы заголовочного типа. Однако в целях стандартизации процесса подготовки необходимых для создания теста данных различий между классами, определенными на чистом языке, и классами, использующими расширения, не делается. Теперь, при подключении этого заголовочного файла к тесту, компилятор будет считать исследуемые методы частью API, и к ним можно обращаться напрямую, как и в общем случае. Данный подход также требует некоторого количества ручной работы по 147
поиску объявления класса, но ее объем не идет ни в какое сравнение с предыдущим вариантом. Также в состав LSB входит множество так называемых thunk методов, которые конструируются компилятором и служат для обращения к виртуальным методам, декларированным в классах-предках. В силу их сугубо вспомогательной природы и отсутствия документации по ним такие операции считаются не подлежащими тестированию и исключаются из дальнейшего рассмотрения.
2.5. Среда исполнения тестируемого метода Одним из ключевых компонентов приложения, написанного с использованием библиотеки Qt, является объект класса QApplication. Он содержит основной цикл обработки сообщений и служит для глобальной инициализации и финализации программы, в частности, если требуется, обеспечивает приложению доступ к графической подсистеме, задавая активный дисплей, визуальный и цветовой контексты. Также класс QApplication позволяет обращаться к таким параметрам системы как шрифты, палитра, интервал двойного нажатия, и параметрам, переданным приложению. Каждое приложение с графическим интерфейсом, использующее Qt, должно иметь лишь один объект этого класса. Система Qt не позволяет создавать объекты классов, осуществляющих вывод какого-либо изображения, до тех пор, пока не проинициализирован объект QApplication. Существует также ряд методов, функциональность которых проявляется только после того, как программа начнет обрабатывать события, происходящие в ней самой и в операционной системе. Поэтому компоновщик добавляет в начало каждого теста конструктор класса QApplication, а в конце теста вызывает метод exec полученного объекта, который запускает цикл обработки сообщений данного приложения. Из этого правила существует несколько исключений. Поскольку в приложении может существовать лишь один объект класса QApplication, то при тестировании его конструкторов и деструкторов попытка создать дополнительный объект приведет к падению теста. Класс QEventLoop описывает основной цикл обработки сообщений, поэтому его объект должен быть сконструирован до объекта QApplication. Также, статический метод самого класса QApplication setColorSpec() влияет на инициализацию графической подсистемы и должен вызываться до начала ее работы, а значит до того, как будет вызван конструктор класса QApplication. Разумеется, многие классы, не затрагивающие непосредственно графический интерфейс, могут работать и без привлечения объекта QApplication, поэтому дополнительная инициализация и вход в цикл обработки сообщений для их тестирования излишни. Однако библиотека Qt преимущественно используется для создания приложений, имеющих графический интерфейс, 148
поэтому подобная избыточность, напротив, приближает среду, в которой вызывается тестируемая операция, к реальной.
2.6. Сигналы и слоты В библиотеке Qt передача сообщений между объектами приложения осуществляется посредством механизма сигналов и слотов. Вызов методасигнала объекта отправляет в основной цикл обработки сообщение, содержащее параметры этого сигнала, которое перехватывается и обрабатывается методом-слотом другого или того же самого объекта. Канал связи между методами определяется макросом connect. Функциональность сигнала заключается в передаче слоту параметров через сообщение, неявно включая указатель на передающий объект, который может быть получен при помощи вызова QObject::sender() в теле слота. Именно ее и нужно проверять в процессе тестирования. В тесте формируется дополнительный объект, имеющий метод-слот с такими же, как и у сигнала, параметрами. Реализация этого метода проверяет значения пришедших параметров на эквивалентность посланным, а также неравенство нулю указателя на передающий сообщение объект, и выполняет выход из приложения. Таким образом, если сообщение не дошло до слота, то приложение не выйдет из основного цикла и будет завершено аварийно. Слот представляет собой обычный метод, работающий с переданными ему параметрами, но может дополнительно обращаться к объекту, пославшему сообщение, посредством глобальной переменной. Поэтому не все реализации методов-слотов будут работать вне контекста передачи сообщения. Для тестируемого слота подбирается существующий в системе подходящий сигнал с таким же, как и у слота, набором параметров. В тесте конструируется объект, имеющий этот метод-сигнал, который при помощи макроса связывается со слотом. Тестовое воздействие производится путем вызова сигнала с проинициализированными параметрами.
2.7. Завершение теста и отложенное выполнение целевого воздействия В подавляющем большинстве случаев приложение, осуществляющее обработку сообщений, завершает свою работу по получении некоторого внешнего сигнала. Например, приложение, имеющее графический интерфейс, в отсутствие исключительных ситуаций работает до тех пор, пока пользователь явно не даст команду на его закрытие. Симуляция такого воздействия выходит за рамки тестирования работоспособности, поэтому завершение работы осуществляется тестом самостоятельно. При помощи сигнала singleShot класса QTimer через определенное время после запуска приложения вызывается слот quit класса QApplication, что и приводит к выходу из цикла обработки сообщений. Тесты запускаются параллельно, поэтому значительных задержек из-за 149
относительно большого времени жизни каждого из них в самом процессе тестирования не возникает. Таким образом, если он завершился самостоятельно в отведенные временные рамки, и при этом не возникло ошибок, то тест считается прошедшим. Если же он не завершился в нужное время, то тест уничтожается загрузчиком, и считается, что тест не прошел. Объекты классов-наследников QDialog имеют свой собственный локальный цикл обработки сообщений. Если локальный цикл получает управление до того, как будет осуществлен вход в основной цикл, то сигнал о завершении работы, испускаемый самим приложением, не будет обработан. Возникает необходимость воздействия на диалоговое окно со стороны внешнего источника. В дополнение, некоторые методы не могут работать в основном режиме до тех пор, пока не запущен механизм обработки сообщений приложения. Поэтому возникает необходимость тестировать ряд операций уже после того, как управление передано основному циклу. Все содержимое теста, необходимое для вызова такой целевой операции, переносится в метод-слот дополнительно сгенерированного класса. С помощью сигнала singleShot этот слот вызывается через некоторое время после запуска приложения, но до его завершения.
3. Дополнительное тестового набора
обеспечение
корректности
Описанные в предыдущем разделе техники, дополнительные к базовому подходу технологии Azov, позволяют формировать простейшие тесты работоспособности для методов классов библиотеки Qt. Однако, несмотря на то, что процесс генерации тестов полностью автоматизирован, из-за очень большого объема работ неизбежно возникают несоответствия в уточняющей информации, ошибочно внесенные разработчиками, которые могут привести к ошибкам в тестовом наборе. Например, нужный специализированный тип может отсутствовать или вместо нужного может быть указан другой. Разработчик, разумеется, просматривает и запускает тесты, над которыми он работает в данный момент, на предмет ошибок, но обнаружить их таким способом удается далеко не во всех случаях. В частности, одна из особенностей системы генерации состоит в том, что полный целевой тестовый набор генерируется каждый раз заново, и в зависимости от того, какие уточнения типов были внесены в базу данных, содержимое уже проверенных тестов может меняться. Возникает необходимость в инструментах, позволяющих выполнять верификацию корректности сгенерированных тестов в указанном в предыдущем разделе смысле, причем в автоматическом режиме.
150
3.1. Проверки, добавляемые автоматически Компоновщик тестов может гарантировать только синтаксическую корректность составляемого им кода, основываясь при этом на знании сигнатур операций. Для построения параметров, которые должны приводить к сценарию нормального использования целевой операции, этой информации недостаточно, и разработчик должен пополнять ее, внося соответствующие уточнения в виде специализированных типов. Если же при автоматической инициализации параметра был использован неподходящий специализированный тип или нужный специализированный тип не был описан на момент сборки, то полученный тест может не достигать поставленных целей. Полностью исключить ошибочные ситуации автоматически невозможно, однако проверка промежуточных условий корректности везде, где это возможно, повышает уверенность в корректности теста в целом. Требование, проверка которого вставляется в тесты компоновщиком по умолчанию, заключается в неравенстве нулю указателей на объекты, расположенные в общей памяти. Фактически, все возвращаемые методами при инициализации параметров указатели на объекты проходят такую проверку. Более конкретные, и потому возможно более полезные, требования задаются разработчиками для каждого типа данных в отдельности и оформляются в виде так называемых общих специализированных типов. В качестве примера таких требований можно привести проверку свойств isNull() и isValid(), которые имеются у большого числа классов и характеризуют полноценность объекта. Недостатком этого подхода является необходимость применения дополнительного специализированного типа там, где автоматически внесенное ограничение может быть нарушено из разумных соображений.
3.2. Контроль корректности наложенных ограничений Дополнительные проверки, описанные в разделе 3.1, позволяют выявлять ошибки только в тех последовательностях инициализации данных, которые формируются самим компоновщиком. Однако уточнения, вносимые разработчиком, также могут быть некорректными. Обычно, код, генерируемый на основе специализированных типов, синтаксически верен и выполняет возложенные на него функции, поэтому основным источником ошибок является указание неверного специализированного типа для параметра операции. Проверка наложенных ограничений выполняется статически при помощи дополнительного инструмента, реализованного в инструменте разработчика, который просматривает базу данных с уточненной информацией о тестируемых операциях на предмет потенциально опасных случаев. Дополнительные возможности обеспечиваются особыми правилами для 151
наименований специализированных типов, которые позволяют классифицировать их по назначению, например, тип, проверяющий возвращаемое значение и имеющий префикс R_, не должен уточнять неизменяемый параметр метода. В частности специализированный тип, указанный для возвращаемого операцией значения, должен содержать некоторую проверку этого значения, а также иметь соответствующее название. Обратно, уточнение для параметров, которые не могут быть изменены в результате вызова целевой операции, не должно содержать проверки возвращаемого значения. Не характерно наличие специализированных типов, уточняющих объекты, для статических методов, конструкторов и деструкторов классов, хотя иногда они необходимы для того, чтобы правильно проинициализировать среду исполнения теста. Также проверяется наличие общих специализированных типов, то есть таких, которые используются по умолчанию, для простейших типов данных, таких как int, bool, char и т.д., поскольку это приводит к массовым нарушениям ранее заданных ограничений на возвращаемые значения методов. Специализированные типы для типов более сложной организации чаще описывают правильную последовательность инициализации объекта, а не конкретное его значение, поэтому для них такая проверка не требуется. Дополнительно имеется возможность поиска дублирующихся или очень похожих специализированных типов, у которых совпадают все поля кроме названия и быть может исходного типа, а также типов, для которых не описано никаких ограничений. Наличие таких типов иногда допустимо, поскольку они позволяют изменять тип конструируемого объекта. Это позволяет подавать в качестве параметра объект типа наследника вместо родителя, при этом, не определяя вручную инициализирующую последовательность. Для защиты системы от случайных ошибок вводится понятие одноразовых специализированных типов. Такой тип, как правило, содержит уникальную проверку или инициализацию, применимую только к одной определенной операции и более нигде не используемую. Как следует из его названия, он может использоваться лишь однажды, и система не позволяет другим разработчиком применить его по ошибке, попутно облегчая им поиск более подходящих специализированных типов. Аналогично, одноразовыми специализированными типами не может пользоваться и компоновщик при инициализации параметров других операции.
4. Оптимизация разработки тестов Ключевым достоинством технологии Azov является возможность достичь чрезвычайно высокой производительности при разработке тестового набора. Затраты на создание одного теста уменьшаются при разработке тестов для 152
интерфейсов большого размера (свыше 1000 операций). Основные трудозатраты приходятся на задачи, выполняемые разработчиком вручную, а именно изучение стандарта, формирование необходимых специализированных типов и отладка полученных тестов. Учитывая огромное количество подлежащих тестированию операций, даже небольшое уменьшение времени разработки теста в среднем приводит к значительному выигрышу для всего тестового набора в целом. Поэтому среда разработки должна иметь гибкий и удобный интерфейс, а также обеспечивать широкие возможности переиспользования уже созданных специализированных типов данных.
4.1. Приведение типов и наследование Специализированный тип фактически является композицией исходного типа данных и некоторого набора ограничений, то есть, по сути, состоит из достаточно независимых компонент. Эту особенность можно использовать для гибкого формирования новых специализированных типов из уже имеющихся в системе. В текущей реализации специализированный тип имеет шесть атрибутов: исходный тип, к которому применяются ограничения, значение типа, блоки дополнительной инициализации и финализации параметра, блок проверки требований и блок вспомогательного кода. Доступ к объекту осуществляется через шаблон, разворачиваемый впоследствии компоновщиком, поэтому все атрибуты практически независимы, за исключением того, что при работе с параметром предполагается, что его тип соответствует исходному типу данных. В системе реализовано однократное наследование специализированных типов. Каждому типу может быть сопоставлен один родитель. Если тип-наследник не содержит описания какого-либо из блоков, то используется описание этого блока у его непосредственного предка. Однако если последний в свою очередь заимствует соответствующее описание у своего предка, то в результате блок наследника окажется пустым. При таком подходе создавать цепочки наследования не имеет практического смысла, и в процессе разработки это, как правило, и не требуется. С другой стороны весьма полезно иметь некий базовый специализированный тип, обеспечивающий основную инициализацию, а уже на его основе строить типы, проверяющие требования. Тогда, при необходимости изменить инициализирующий код, это нужно будет сделать только в одном месте. Существующий механизм полезно в дальнейшем дополнить наследованием от двух родителей с одинаковым исходным типом одновременно. От одного родителя можно будет взять инициализирующий код, от второго – проверку возвращаемого значения, а в самом наследнике определить специфичную для тестируемой операции инициализацию и финализацию. Это позволит несколько сократить время разработки теста, а также изменять меньшее 153
количество специализированных типов при необходимости внесения поправок. Специализированные типы для производных типов, таких как указатели, ссылки, синонимы (typedef), константы (const), а также конструкции более сложной структуры, можно создавать автоматически с помощью механизма неявного наследования. Производные одного типа образуют класс эквивалентности в том смысле, что ограничения специализированного типа для одного из них могут быть применены к любому другому типу из этого множества. Дополнительные поправки в цепочку инициализации параметра автоматически вносятся компоновщиком при сборке теста. С точки зрения пользователя системы это выглядит так, как будто для всех параметров, имеющих типы из одного класса эквивалентности, существуют специализированные типы одинакового назначения. В качестве направления дальнейшего развития этого механизма можно предложить заимствование иерархии наследования самого языка C++. То есть, например, специализированный тип, уточняющий объект класса-наследника, может использоваться при инициализации параметра, имеющего тип классародителя. Также полезно ввести неявное наследование для произвольных определяемых пользователем групп классов. В частности, целесообразно переиспользовать специализированные типы данных для классов, имеющих похожие методы, например, классов-наследников одного и того же родителя, немного поразному реализующих сходную функциональность.
4.2. Удобство и функциональность интерфейса Инструмент разработчика тестов в первую очередь предназначен для доступа к базе данных с дополнительной информацией о тестируемых операциях, а именно позволяет создавать и редактировать специализированные типы, просматривать состояние работ в целом и для каждой операции в частности, а также собирать статистические данные. Скорость разработки тестового набора существенно зависит от удобства этого инструмента и его функций, позволяющих сократить время на выполнение рутинных задач. Написание теста можно существенно упростить, если предоставить разработчику легкий доступ к справочной и отладочной информации. Поэтому каждая операция сопровождается ссылкой на ее описание в стандарте. Инструмент также позволяет просматривать сгенерированный компоновщиком код, запускать тесты на целевой машине и получать подробную информацию об ошибках, возникших на этапах генерации, компиляции и исполнения. Возможны ситуации, когда несколько операций реализуют сходную функциональность и при этом имеют одинаковую сигнатуру, так что набор специализированных типов, которыми необходимо уточнить параметры, для них один и тот же. Разумно определить уточнения только для одной из таких 154
операции, а затем автоматически их дублировать. Для этой цели существует инструмент импортирования, который позволяет производить поиск таких случаев сразу для всех методов класса и выбирать источник копирования из списка возможных кандидатов. Дополнительно, с помощью него можно автоматически создавать копии существующих специализированных типов с изменением типов уточняемых ими параметров. Быстрому поиску подходящих для подстановки специализированных типов служит система их названий. Название должно нести в себе краткую информацию об ограничениях, содержащихся в типе, например, содержать начальное значение параметра, если таковое имеется, или префикс R_, если выполняется проверка возвращаемого значения. Общеупотребительные типы принято называть с помощью имен, начинающихся с Create, в этом случае инструмент импортирования в одном из режимов работы дублирует их автоматически. Ряд специализированных типов несет в себе уточнения, имеющие смысл только для одной определенной операции. Для того чтобы облегчить поиск подходящих типов, а также предотвратить их ошибочное переиспользование, такие специализированные типы отмечаются как одноразовые и скрываются из списка доступных. Возможна и обратная ситуация, когда требуется указать один и тот же специализированный тип для всех вхождений какого-либо типа данных, за исключением единичных случаев. Такой тип объявляется общим и используется при генерации тестов по умолчанию, если явно не указан другой. При создании инициализирующей последовательности вызовов для специализированного типа ее параметры не обязательно конкретизировать. В этом случае там, где это нужно, указывается символическая конструкция, на место которой компоновщик подставляет правильно построенный объект, возможно используя при этом какой-нибудь специализированный тип. В некоторых случаях процесс описания специализированного типа можно упростить, используя шаблоны кода для часто встречающихся случаев. Например, применяется вставка вызовов всех методов, проставляющих атрибуты объекта, названия которых в Qt начинаются на set.
4.3. Разбиение на группы и порядок выполнения работ В процессе разработки пользователь может столкнуться с необходимостью проинициализировать параметр типа, для которого еще никто не составил подходящих специализированных типов. В этих случаях разработчику требуется отвлечься на время от текущей задачи, изучить соответствующий раздел стандарта и описать нужные уточнения для типа данных или подождать, пока другой разработчик сделает это, что зачастую занимает весьма значительное время. При разработке тестов для большого числа операции постоянное отвлечение на другие задачи сильно замедляет создание 155
тестов, поскольку разработчику придется позже восстанавливать свои знания об отложенной на время операции. Для того чтобы минимизировать время на переключение контекста в процессе разработки, используется инструмент построения расписания работ. Множество операций, для которых создается тестовый набор, разбивается на группы в соответствии с их назначением, что позволяет разработчикам изучать один и тот же раздел стандарта сразу для целой группы функций или классов. Затем для каждой группы и для каждой операции внутри группы определяется приоритет, согласно которому следует вести разработку. Применительно к C++ удобнее оперировать не в терминах отдельных операций, а в терминах целых классов. Внутри группы каждому классу ставится в соответствие вес, равный разности между количеством классов этой же группы, имеющих методы, в которых этот класс выступает в качестве параметра, и количеством классов группы, которые используются в его методах. Чем больше вес класс, тем раньше следует создать уточняющего его специализированные типы. На практике это означает, что более высокий приоритет имеют низкоуровневые и вспомогательные классы, которые затем используются более сложными. На уровне групп веса рассчитываются таким же образом, с учетом всех входящих в группу классов. Однако здесь приоритеты имеют более общий смысл и отражают то, насколько часто в тестируемом интерфейсе используется та или иная подсистема.
5. Достоинства и недостатки подхода Описанная в предыдущих разделах реализация системы разработки тестов, основанная на технологии Azov, была применена при создании тестового набора для интерфейса библиотеки Qt3. Тестированию подлежало около 10000 операций, поведение которых описывается стандартом LSB. При этом практически во всех случаях удалось построить работоспособные простейшие тесты. На основе полученного опыта можно сделать выводы о преимуществах и недостатках технологии в целом и данной ее реализации. Технология построения простейших тестов работоспособности действительно позволяет создавать тестовые наборы, решающие поставленную задачу для интерфейсов большого размера. Эффективность ее применения возрастает в тех случаях, когда интерфейс содержит небольшое количество существенно различных типов данных по сравнению с общим числом операций в нем. Это можно увидеть на примере подсистемы работы с базами данных QSql, которая содержит 447 операций из LSB (501 всего), 20 собственных классов и 7 общеупотребительных типов данных. При этом работающие тесты можно построить даже при отсутствии документации на часть операций, что для библиотеки Qt более чем актуально. 156
В результате расширения базовой методики технологии Azov простейшие тесты удалось создать для приблизительно 99,5% объема входящей в LSB части интерфейса Qt. Лишь около 35 операций не получилось протестировать по причине простоты тестов или критической нехватки документации. При этом в процессе описания ошибок проявилась неожиданная сложность. Дело в том, что при отсутствии сколько-нибудь подробной документации чрезвычайно трудно отличить случаи неверного использования операции от случаев обнаружения настоящих ошибок в ее реализации. Фактически для принятия решения в этих случаях приходится изучать исходный код операций. В существующем виде технология не предполагает реализацию зависимости между параметрами операции и возвращаемым ею значением. Поэтому в общем случае допустимы только проверки общего вида (например, метод должен вернуть неотрицательное число), которые далеко не всегда можно указать. Однако в целях повышения аккуратности тестового набора в тех случаях, когда указаны конкретные значения параметров, влияющие на результат, при помощи специализированного типа проверяется равенство результата операции конкретному правильному значению. Построение автоматических сценариев путем варьирования значений параметров операций может усложнить проверку возвращаемых значений. Вероятно, основные возможности для улучшения тестов заключаются в задании некоторого множества значений для каждого специализированного типа и построении отображений между исходными данными одних и проверками других. Что касается среды разработки тестов, то она получилась достаточно удобной и простой в использовании. Ее изучение сводится к прочтению нескольких страниц руководства и небольшой самостоятельной практике. К наиболее существенным недостаткам интерфейса относится отсутствие возможности выполнить какие-либо действия между вызовом целевой операции и проверкой возвращенного ею значения. Такие случаи возникают, когда перед получением результата надо произвести дополнительные действия над объектом целевого воздействия. Приходится добавлять такие вызовы непосредственно в условие проверки правильности результата. Из-за особенностей механизма неявного наследования в некоторых случаях приходится создавать специализированные типы, в которых тип фактически инициализируемых данных не соответствует уточняемому типу. Подобная ситуация возникает, например, когда требуется использовать в качестве параметра объект класса-наследника вместо базового класса, указанного в сигнатуре операции. При этом корректное приведение типов моделируется компоновщиком неявно на основе эвристики, что запутывает разработчика и усложняет процесс генерации теста. В том случае, когда для нормальной работы операции необходима явная зависимость между ее параметрами, технология Azov предлагает 157
использование комплексных специализированных типов, которые совместно уточняют сразу несколько параметров. Было реализовано сразу два способа задания таких конструкций. Во-первых, можно явным образом создать особый тип, уточняющий типы сразу нескольких параметров операции. Однако получающаяся конструкция выглядит слишком искусственно, поскольку зависимости между параметрами записываются чаще всего при помощи глобальной функции или переменной. Второй вариант – объединить два обычных специализированных типа путем указания в них ссылок друг на друга Недостатком в данном случае неочевидная зависимость между типами, поскольку используются ссылки на номер параметра операции. Возникает потребность обобщить модель специализированных типов с учетом особенностей разных языков программирования, по меньшей мере, реализовать более полное явное и неявное наследование, а также формализовать и улучшить использование ссылочных отношений, указывающих как на номер параметра, так и на другой тип данных.
6. Заключение В статье описываются техники, позволяющие адаптировать общий подход технологии Azov для создания тестов работоспособности для классов языка C++ и библиотеки Qt3. Большинство из таких техник могут быть использованы и в других системах. На их основе была реализована система генерации тестов, позволившая с приемлемыми усилиями построить тестовый набор для 10000 методов и функций из библиотеки Qt3, описанных в стандарте LSB. При этом, несмотря на неполноту документации, производительность разработки тестов составила в среднем 70 операций в день на человека. При помощи простейших тестов работоспособности удалось обнаружить порядка 10 ошибок в реализации методов. В процессе разработки тестового набора удалось обнаружить и исправить достаточно большое количество ошибок в базе данных LSB, а также пополнить ее недостающей информацией. Таким образом, показана применимость на практике и высокая эффективность технологии Azov, пополненной набором описанных в данной работе техник. Литература [1] Р. Зыбин, В. Кулямин, А. Пономаренко, В. Рубанов, Е. Чернов. Технология Azov автоматизации массового создания тестов работоспособности. Опубликовано в этом же сборнике. [2] http://www.linuxbase.org. [3] http://www.linux-foundation.org/navigator/commons/welcome.php. [4] http://doc.trolltech.com/3.3/index.html. [5] А. Пономаренко, Е. Чернов. Алгоритм генерации тестов работоспособности на основе расширенной базы данных LSB. Опубликовано в этом же сборнике.
158
Автоматизация тестирования webприложений, основанных на скриптовых языках Д. В. Силаков [email protected] Аннотация. Статья рассказывает о подходе к тестированию Web-приложений, основанных на скриптовых языках, позволяющем за короткое время создать достаточно качественный набор тестов. Описываемый подход основан на анализе исходного кода (т.е. относится к методам тестирования на основе «белого ящика») и использует некоторые особенности кода приложений, предназначенных для работы в Web. Рассматриваются возможные процедуры вынесения вердикта об успешности прохождения теста, не требующие вмешательства человека. Также описано применение предложенной методики к тестированию LSB Навигатора — Webинструмента для просмотра и анализа содержимого спецификационной базы данных стандарта Linux Standard Base, разрабатываемого в ИСП РАН в рамках совместного проекта с Linux Foundation.
1. Введение В настоящее время все большее распространение получают приложения, взаимодействующие с пользователем посредством Web-интерфейса. Такие приложения не занимаются непосредственно отображением пользовательского интерфейса, а перекладывают эту задачу на посредника — как правило, в этой роли выступает Web-браузер, получающий от приложения документы в формате HTML [1], отображающий их в своем окне, и передающий приложению информацию о различных действиях, совершаемых пользователем. В зависимости от этих действий приложение создает и передает Web-браузеру новую страницу HTML, и работа продолжается дальше. Страницы HTML, как правило, генерируются приложением в процессе работы; при этом могут использоваться различные шаблоны, задающие стиль и структуру документа, в то время как содержательная часть создается динамически. При наполнении страницы может использоваться некоторое хранилище данных (в роли которого, как правило, выступает база данных). Такой подход широко распространен в системах управления информацией (Content Management Systems, CMS), различных интернет-форумах, и т.п. 159
Отделение данных от остальной инфраструктуры приложения обеспечивает возможность удобно и быстро изменять содержимое интернет-порталов и сайтов; в то же время изменение оформления или структуры страниц не требует какой-либо работы с данными. Для написания программ, генерирующих страницы HTML с использованием данных из внешнего хранилища, широко используются скриптовые языки программирования [2], например, Perl и PHP. Такие языки являются интерпретируемыми, что позволяет программистам не беспокоиться о том, на какой программно-аппаратной платформе будет работать приложение (естественно, при условии, что для данной платформы существует интерпретатор соответствующего языка). Как и для любого программного обеспечения, для Web-приложений важным является вопрос обеспечения их качества — программы должны выдавать те страницы, которые ожидает пользователь. Таким образом, необходимо решать задачу функционального тестирования таких программ (других важных аспектов проверки качества Web-приложений, например, нагрузочного тестирования, мы в этой статье касаться не будем). Кроме того, большинство Web-приложений постоянно развиваются и модифицируются, поэтому важным является наличие регрессионных тестов, позволяющих удостовериться, что в результате внесения изменений функциональность приложения не нарушается. Непосредственное тестирование Web-приложения человеком (заключающееся, фактически, в переходе по различным ссылкам внутри приложения и анализа отображаемых страниц) отнимает много времени и в случае больших приложений малоэффективно. Под «большими» здесь стоит понимать программы, не только содержащие много строк кода, но и обладающие большим количеством входных параметров, работающие с базами данных сложной структуры и с большим количеством записей. При разработке таких программных продуктов вопрос об автоматизации процесса их тестирования стоит особенно остро.
2. Существующие подходы к тестированию Webприложений Большинство из существующих подходов к тестированию Web-приложений являются тестированием по принципу "черного ящика". Все, чем обладает тестировщик — это само приложение, с которым можно взаимодействовать, например, через Web-браузер, и список требований, которым приложение должно удовлетворять. Тестирование заключается в переходе по различным ссылкам внутри приложения и анализе получаемых страниц. Существуют инструменты для автоматизации этих процессов, однако ведущая роль в разработке тестов все-таки отводится человеку. 160
Существует много решений, позволяющих записывать сценарии поведения пользователя (т.е. цепочку ссылок, по которым осуществлялся переход) — IBM Rational Robot [3], HP WinRunner [4], Empirix e-TEST [5] и другие. Записанный единожды сценарий может далее воспроизводиться автоматически. Однако создание сценариев — трудоемкое занятие, причем отдельной задачей является анализ требований к приложению с целью определить, какие именно сценарии должны быть созданы для обеспечения хорошего качества тестирования. Некоторые инструменты (например, компонент PureAgent в системе PureTest [6]) позволяют создавать сценарии на основе действий реальных пользователей, работающих с приложением. Однако и при таком подходе при достаточно большом количестве пользователей встает вопрос о выборе из множества возможных сценариев относительно небольшого набора, который, тем не менее, обеспечит хорошее качество тестирования. Существуют инструменты, позволяющие автоматически генерировать ссылки для обращения к приложению по протоколу HTTP, получать соответствующие страницы и производить их анализ. Однако при генерации ссылок также обычно применяется подход «черного ящика» — исходный код приложений не анализируется, а ссылки, которые необходимо генерировать, должен описать тестировщик с использованием специфических для каждого инструмента средств. Например, eValid [7] позволяет перебирать значения параметров для скриптов, которые будут подставляться в создаваемые ссылки, но список параметров с возможными значениями для каждого скрипта должен составлять тестировщик. При достаточно большом количестве скриптов и параметров создание такого списка потребует много времени; кроме того, список должен постоянно поддерживаться в актуальном состоянии — тестировщик должен следить за изменением состава скриптов и их параметров. Инструмент Puffin [8] позволяет генерировать для параметров произвольные значения, однако такой подход во многих случаях сильно снижает качество тестирования по сравнению с ручным заданием значений. В случае Puffin, опять же, список имен параметров должен составлять и поддерживать тестировщик. Кроме того, существующие инструменты не предоставляют удобных средств для перебора различных комбинаций параметров, а во многих случаях интерес представляет именно перебор комбинаций, поскольку разные сочетания параметров могут задействовать различные части приложения. Многие инструменты анализируют получаемые в процессе тестирования страницы, извлекая из них ссылки на другие части приложения и имитируя переход по ним (опять же, с дальнейшим анализом получаемых страниц; например, для таких целей предназначен компонент 'Web Crawler', входящий в PureTest). Таким образом, осуществляется переход по всем ссылкам, которые могут быть достигнуты, начиная с определенной страницы. Поскольку все части приложения, как правило, взаимосвязаны, то в идеале, начав с некоторой стартовой страницы и посетив все достижимые из нее ссылки, 161
можно протестировать всю функциональность приложения, доступную пользователю. Однако число ссылок может быть чрезвычайно велико, и лавинообразно расти с увеличением числа посещенных страниц. К сожалению, современные средства перехода по ссылкам достаточно примитивны и просто осуществляют переходы по всем встреченным ссылкам (ввиду чего они часто используются при нагрузочном тестировании [9]). Настройка либо доработка инструментов для более «интеллектуального» выбора ссылок, по которым надо осуществлять переходы, требует тщательного анализа самого приложения. Проверка правильности может производиться путем сравнения получаемых страниц с эталонными. Такие проверки полезны при регрессионном тестировании, однако если что-то изменилось в структуре страниц с момента создания эталонов, то эталоны должны быть созданы заново, а проверка корректности страницы — произведена вручную. Кроме того, во многих случаях стоит цель тестирования не только части приложения, занимающейся формированием страниц HTML, но всего программного комплекса, неотъемлемой частью которого является постоянно изменяющаяся база данных. Сравнение страниц целиком в таких случаях не оправдано. Например, в случае приложения, формирующего страницу с ежечасно обновляемыми новостями, документы, выдаваемые по одному и тому же запросу в различные моменты времени, с большой вероятностью будут отличаться. Можно поддерживать еще и заранее заданный набор данных, на котором проводится тестирование, однако это не избавляет от проблем в случае изменения структуры или оформления страниц. В качестве альтернативы предлагаются менее строгие проверки; так, уже упоминавшийся eValid позволяет производить более 20 сравнений, среди которых можно отметить следующие: 'URL' — проверка ссылки, на которой оказался пользователь после совершения определенных действий, записанных в сценарии; 'Title' — проверка названия страницы; 'Elements' — проверка числа элементов в DOM-модели страницы; 'Byte Size' — проверка размера страницы; 'Last Modified Date' — проверка даты последнего изменения страницы; 'Checksum' — проверка контрольной суммы для текста страницы; 'Text' — проверка выделенных участков текста страницы; 'Screen Rectangle' — сравнение изображения определенного участка страницы с тем, что наблюдал тестировщик во время записи сценария. Остальные проверки являются вариациями проверки части страницы как изображения либо являются комбинациями указанных проверок. Можно отметить, что несмотря на достаточно большое количество доступных проверок, ни одна из них не пригодна в случае, когда изменяется 162
содержательная часть страницы — часть проверок (такие, как 'Title' и 'URL') могут вообще не зависеть от этой составляющей документа, а другие ('Byte Size', 'Checksum', 'Screen Rectangle') с большой вероятностью сообщат об ошибке (т.е. об отличии полученного на новых данных результата от эталонного), но такие сообщения скорее всего не будут свидетельствовать о реальном нарушении функциональности приложения. Существуют и более сложные подходы к разработке тестов для Webприложений, позволяющие абстрагироваться от различных аспектов, связанных непосредственно с языком разметки HTML, и генерировать тесты на основе формальной модели данных, передаваемых приложению — см., например, [10]. Однако такие подходы трудоемки и требуют достаточно высокой квалификации тестировщиков. В то же время исходный код Web-приложений, основанных на скриптовых языках, обладает рядом особенностей, позволяющих на основе его анализа автоматически генерировать ссылки для тестирования, что и будет продемонстрировано в данной статье. При этом соответствие кода некоторым условиям существенно повышает качество создаваемых тестов, что может быть учтено при разработке приложения. Также будут рассмотрены методы и инструменты, которые можно применять в процессе анализа генерируемых страниц для вынесения вердикта об успешности тестирования. Основная цель предлагаемого подхода — быстрое создание тестового набора, покрывающего достаточно большую часть функциональности приложения, не требующего больших затрат по поддержанию тестов в актуальном состоянии, но в то же время способного выявлять достаточно большой спектр ошибок.
3. Использование тестов
исходного
кода
для
генерации
3.1. Извлечение имен параметров и их значений Как уже было отмечено ранее, при работе с Web-приложением пользователь непосредственно взаимодействует с посредником в виде Web-браузера. Браузер, в свою очередь, взаимодействует с Web-сервером, на котором работает приложение, по протоколу HTTP (конечно, возможно использование других посредников и других протоколов, однако они применяются достаточно редко, и здесь мы их рассматривать не будем). Для передачи данных Web-браузера серверу протоколом HTTP [11] предусмотрено несколько методов передачи параметров, из которых в большинстве Webприложений используются два — GET и POST. Параметры GET — это параметры, передаваемые непосредственно в адресной строке Web-браузера. Параметры POST передаются вместе с пакетами данных (и используются, как правило, либо для передачи больших объемов данных, поскольку не имеют ограничений на размер, либо чтобы не загромождать адресную строку браузера). 163
Чтобы протестировать приложение, необходимо знать, какие имена параметров оно ожидает увидеть в запросе и какие значения должны принимать эти параметры, чтобы выполнилась та или иная часть программы. Для выполнения этих задач необходим анализ потока данных в приложении. В общем случае эта задача нетривиальна и требует использования методов статического анализа кода; однако для каждого конкретного приложения с большой вероятностью такой анализ может быть достаточно простым. Тем более что при создании тестов не обязательно задаваться целью проанализировать все возможные варианты поведения приложения в зависимости от входных данных — следует исходить из желаемого соотношения качества тестов ко времени и ресурсам, необходимым для их разработки. Сложность создания инструмента для анализа потока данных зависит от структуры исходного кода приложения. Как будет показано ниже, во многих случаях за короткое время можно создать инструмент, производящий достаточно подробный разбор потока данных. Каждый скриптовый язык, рассчитанный на применение в Web-приложениях, предоставляет программистам удобные и унифицированные способы доступа к параметрам запроса. Так, в языке PHP доступ к параметрам, переданным скрипту методами POST и GET, обычно осуществляется через ассоциативные массивы $_POST и $_GET соответственно (или с помощью их устаревших аналогов, $HTTP_POST_VARS и $HTTP_GET_VARS), либо с использованием массива $_REQUEST, который в дополнение к таким переменным содержит пользовательские данные, передаваемые Web-браузером — так называемые cookie. Ключами в этих массивах являются имена параметров; в ответ на обращение по ключу возвращается значение соответствующего параметра. Аналогичный подход с использованием ассоциативных массивов применяется в Perl. Таким образом, в случае PHP для определения имен параметров, воспринимаемых скриптом, достаточно выделить из текста скрипта использования массивов $_REQUEST, $_POST и $_GET, и проанализировать, по каким ключам производится выборка из этих массивов. В простейшем и самом распространенном случае ключ, по которому производится выбор элемента — это и есть имя параметра. Процесс извлечения таких имен из исходного кода легко автоматизируется. Не стоит при этом забывать, что скрипт может подключать другие файлы; анализ кода подключаемых файлов также может быть востребован. Естественно, обращение к массивам может производиться по ключу, формирование которого производится в процессе работы программы. Сложность определения такого формирования ключей зависит от приложения; стоит или нет пытаться выявить возможные значения ключа в этом случае и насколько глубоким должен быть анализ — решать тестировщикам, исходя из предполагаемой трудоемкости работы и потенциальных выгод, которые она может принести. 164
Однако знание одних лишь имен параметров может и не сильно облегчить задачу создания качественных тестов; к тому же во многих случаях, даже для больших приложений, список имен можно составить и вручную. Гораздо больший интерес представляют возможные значения параметров, ведь при разных значениях одного и того же параметра приложение может вести себя по-разному. Здесь также во многих случаях может помочь исходный код приложения. Одним из способов узнать возможные значения параметров, которые могут влиять на процесс работы, — это анализ условий в операторах ветвления типа 'if' и 'switch' — если при вычислении условия для оператора ветвления присутствует сравнение параметра запроса с некоторой константой, то эта константа и есть интересующее нас значение. Для решения этой задачи необходимо выявить, где в вычислениях условий операторов ветвления используются параметры запроса, а также какие значения являются операндами в операциях сравнения, где один из операндов — параметр запроса. Сложность решения такой задачи также зависит от структуры исходного кода приложения. Для языка PHP можно сказать, что, как и в случае поиска имен параметров, поиск значений проще всего автоматизировать, если при вычислении условий операторов ветвления производится непосредственное обращение к массивам $_REQUEST, $_POST или $_GET, а сравнение производится с константными выражениями. Подводя итоги вышесказанного, можно сказать, что автоматизация процесса поиска имен параметров и их значений в исходном коде приложения, написанном на PHP, является достаточно простой задачей, если выполнены следующие условия: обращение к массивам $_REQUEST, $_POST и $_GET производится непосредственно по именам параметров;
если в вычислении условия для оператора ветвления используется значение, полученное в запросе, то обращение к этому значению производится непосредственно через обращение к массивам $_REQUEST, $_POST и $_GET;
если в операции сравнения один из операндов — параметр запроса, то второй — константа.
Стоит отметить, что для приложений, работающих с базами данных, параметры запроса могут соответствовать некоторым полям из таблиц базы. При этом приложение может вести себя по-разному в зависимости от того, есть ли в базе данных запись, где соответствующее поле имеет значение, равное переданному значению параметра, или нет. Выявить такие ситуации сложно, поэтому можно порекомендовать всегда использовать в тестах как минимум два значения для каждого из параметров — одно из которых есть в базе данных, а другое отсутствует. Поскольку определить, какое значение есть в базе, на основе одного лишь кода программы зачастую невозможно, то здесь необходимо вмешательство человека. Однако и эту задачу можно 165
автоматизировать, если в процессе создания тестов есть возможность доступа к базе данных, с которой работает приложение. В таком случае достаточно указать, какие поля каких таблиц базы соответствуют именам параметров, и возложить задачу непосредственного выбора значений на генератор тестов.
3.2. Зависимости параметров Помимо имен параметров и их возможных значений, полезно знать взаимосвязи между параметрами; это позволяет существенно сократить количество генерируемых тестов без потери качества. Например, если приложение использует параметр param1, только если в запросе присутствует param2, то нет смысла создавать запросы, в которых будет присутствовать param1 и отсутствовать param2. Можно выделить два основных вида зависимостей между параметрами:
значения параметров {a1, a2, ... an} используются, только если в запросе присутствуют параметры {b1, b2, ... bm};
значения параметров {a1, a2, ... an} используются, только если в запросе присутствуют параметры {b1, b2, ... bm} и их значения лежат в интервалах {v1, v2, ... vm} соответственно;
Задача определения всех зависимостей в общем случае также достаточно трудна, однако и здесь для каждого конкретного приложения могут существовать простые способы определить существенную часть зависимостей (достаточно большую, чтобы существенно сократить тестовый набор). В большинстве случаев анализ ветвлений (условных операторов и операторов выбора), производимых с учетом значений параметров запроса, с последующим анализом того, какие параметры используются в каждой ветке, может дать неплохой результат. Здесь снова встает вопрос о том, как определить, что некоторый параметр участвует в вычислении условия для ветвления, а также то, что некоторый параметр используется внутри ветки. Т.е. задача схожа с задачей выявления имен параметров и их возможных значений, рассмотренной ранее. Для нее справедливы все приведенные выше утверждения; в частности, можно сказать, что она достаточно хорошо автоматизируется, если исходный код приложения удовлетворяет условиям, перечисленным в конце предыдущего раздела. Зная имена параметров, принимаемых скриптами, их возможные значения и взаимосвязи, можно формировать тестовые запросы, содержащие различные параметры с различными значениями. Однако перебор всех возможных сочетаний значений даже для относительно небольших наборов параметров может привести к слишком большому количеству тестов. Учет взаимосвязей помогает сократить количество тестов, однако этого сокращения может оказаться недостаточно. В таких случаях полезно сделать генератор тестов параметризуемым, чтобы он в зависимости от некоторых настроек выбирал только часть из допустимых тестовых воздействий. Настройки генератора 166
выбираются из соображений качества получаемого тестового набора, а также, зачастую, исходя из времени, требуемого для выполнения всех тестов.
4. Вынесение вердикта приложения
о
правильности
работы
Создание тестовых данных является важным, но не единственным аспектом проверки качества ПО. Любому тесту необходимо уметь выносить вердикт о правильности работы тестируемого приложения. В случае Web-приложений это означает, что для каждой страницы, полученной от приложения по сгенерированной ссылке, необходимо установить, были ли допущены ошибки при ее формировании. Осуществить такую проверку в полном объеме под силу лишь человеку; однако существуют некоторые виды проверок, которые можно выполнять автоматически. Примеры таких проверок, производимые инструментом eValid, приведены во введении; для их более детального описания можно обратиться к документации инструмента [7]. Похожие виды проверок предоставляют многие инструменты, однако в силу своей универсальности они достаточно примитивны и способны выявлять очень узкий класс ошибок. Рассмотрим ряд проверок, которые не столь универсальны (в том плане, что детали их конкретных реализаций могут сильно зависеть от тестируемого приложения), но позволяют производить более глубокий анализ результатов, получаемых в процессе выполнения тестов.
4.1. Ошибки взаимодействия с внешними системами Как отмечалось выше, многие Web-приложения активно используют хранилища данных; кроме того, они могут взаимодействовать с другими внешними системами. Если успешность взаимодействий является критичной для функциональности приложения, то в процессе тестирования важно выявлять ошибки, происходящие во время обращения к внешним системам либо хранилищам данных. Одним из методов, позволяющих автоматически распознавать такие ошибки во время тестирования, является использование для взаимодействия функций, которые в случае возникновения ошибок выводят сигнализирующее об этом сообщение в специальный журнал либо непосредственно в текст генерируемого документа. Во многих случаях для обращения к внешним объектам используются функции, предоставляемые внешними системами, которые в случае неудачного выполнения возвращают код ошибки либо выставляют переменные среды в определенные значения. Например, для выполнения запросов к базам данных, как правило, используются стандартные функции, принимающие на вход запрос в виде текстовой строки. В этом случае для них могут быть написаны функции-обертки, вызывающие внутри себя реальные функции, анализирующие результат их работы и в случае ошибок 167
добавляющие в журнал либо в генерируемый документ некоторое сообщение. При добавлении сообщения в создаваемый документ необходимо убедиться, что текст сообщения не может появиться в документе в случае нормального функционирования приложения. Тогда появление такого сообщения будет сигнализировать об ошибке. Процесс поиска заданного текста в документе достаточно прост, и многие инструменты тестирования предоставляют такую возможность. Некоторые программы (например, Jmeter [12]) позволяют искать текст, соответствующий определенному регулярному выражению.
4.2. Соответствие спецификации XHTML Web-приложение создает страницы в формате HTML либо XHTML [1]. Стандарт XHTML является более строгим; соответствие страниц этому стандарту облегчает работу Web-браузеров по их отображению и уменьшает время их загрузки. Соответствие спецификации XHTML позволяет избежать ошибок, связанных со структурной разметкой страницы (отсутствие необходимых тегов, использование атрибутов, которые могут поддерживаться не всеми браузерами и т.п.). Стандарт предъявляет строгие требования только к структуре документа, не затрагивая его содержимое; однако, как показывает практика, несоответствие сгенерированной страницы спецификации XHTML часто является следствием некорректной обработки данных, а не ошибкой форматирования страницы (при условии, что шаблоны разметки, используемые для формирования страницы, соответствуют стандарту XHTML). Например, рассмотрим приложение, которое выводит текстовые данные, внутри которых могут содержаться угловые скобки — '<' и '>' (например, декларации шаблонов C++). Если угловые скобки печатать в документ HTML «как есть», то браузер будет воспринимать текст между двумя парными скобками как тег, и отображать его не будет. Однако поскольку такого «тега», скорее всего, не существует (не предусмотрено спецификацией XHTML), то при проверке на соответствие документа XHTML будет выявлена ошибка. Для проверки соответствия документа спецификации XHTML существуют свободно доступные инструменты, например, Offline HTMLHelp.com Validator [13].
4.3. Анализ лог-файлов Web-сервера Важной особенностью работы Web-приложений является то, что сообщения об ошибках, генерируемые интерпретатором скриптов могут (при соответствующих настройках) автоматически записываются в лог-файл HTTPсервера. Среди ошибок могут быть и такие, которые не мешают интерпретатору продолжить выполнение скрипта, но могут привести к некорректности получаемых страниц. Одной из самых распространенных ошибок такого рода в случае PHP и Perl является выход за границы массива в 168
случае обычных массивов либо отсутствие значения для ключа в ассоциативном массиве. Например, в следующем коде $select = 'SELECT Aid, Aname FROM Application'; $res = Query($select); while( $row = mysql_fetch_array($res) ) { print 'Application '.$row['Aname'].' '.$row['Aversion']; } осуществляется попытка вывести на страницу значения полей Aname и Aversion из таблицы Application, но поле Aversion в запросе отсутствует. В результате будет выведено только поле Aname, а это не совсем то, что хотел программист. При этом никаких ошибок работы с базой данных не будет, также не будет нарушено соответствие страницы стандарту XHTML. Помимо непосредственного просмотра страницы обнаружить эту ошибку можно, просмотрев логи Web-сервера, в которых появится запись вида Notice: Undefined index: Aversion При этом будет указан файл и номер строки, где произошло обращение по некорректному ключу. Таким образом, анализ лог-файлов Web-сервера после выполнения тестов также может служить важной составляющей процесса тестирования.
5. Тестирование LSB Навигатора В рамках совместного проекта ИСП РАН и Linux Foundation по развитию инфраструктуры стандарта LSB в ИСП РАН разрабатывается LSB Навигатор (LSB Navigator) [14] — Web-приложение, позволяющее пользователям в удобной и доступной форме получать и анализировать данные, находящиеся в спецификационной базе данных LSB [15]. LSB Навигатор полностью написан на PHP и в настоящее время представляет собой 120 скриптов, содержащих около 17000 строк кода. В процессе работы может создаваться порядка 250 различных типов страниц. Количество используемых параметров запроса варьируется от 5 до 50 для разных скриптов. База данных LSB содержит 62 таблицы, а также 167 так называемых кэш-таблиц, создаваемых автоматически и используемых для ускорения работы Навигатора. По состоянию на декабрь 2007 года (дата выхода LSB 3.2), обычные таблицы содержат около 18 миллионов записей, кэш-таблицы — около 6 миллионов. Общее количество различных страниц, которые можно получить при работе с приложением, составляет около ста миллионов. LSB Навигатор активно развивается, и ввиду достаточно сложной внутренней структуры программы актуальной является проблема регрессионного тестирования — изменения в некоторых участках кода могут повлиять на десятки страниц, и определение и проверка таких страниц вручную — 169
трудоемкий процесс. Кроме того, для постоянно появляющихся новых возможностей необходимы новые тесты. Применяемый подход позволил за короткое время получить достаточно качественный набор регрессионных тестов. Более того, тесты для новых возможностей генерируются без участия человека; таким образом, регрессионный набор автоматически поддерживается в актуальном состоянии.
5.1. Анализ кода Код LSB Навигатора имеет ряд особенностей, благодаря которым для анализа скриптов в целях генерации тестов не требуется полноценного парсера PHP. Так, каждый скрипт, к которому можно обратиться из адресной строки браузера, имеет следующую структуру: <инициализационная часть; возможно, некоторые проверки параметров запроса> switch( $_REQUEST['cmd'] ) { case "cmd1": function1( $_REQUEST["param1"], $_REQUEST["param2"], ... ); break; case "cmd2": function2( ... ); break; ... case "": default: default_function( ... ); } Для простоты здесь не приведены различные действия с параметрами запроса перед вызовом функций — например, проверка корректности их значений. Кроме того, внутри каждой ветви оператора выбора могут присутствовать вложенные конструкции ветвления и какие-то дополнительные действия, не относящиеся к обработке параметров запроса. Помимо массива $_REQUEST, используются также обращения к массивам $_GET и $_POST. Инициализационная часть также может содержать обращения к этим массивам, однако она, как правило, не содержит ветвлений и проста для анализа. Основная же функциональность скрипта заключается в проверке значения параметра 'cmd' и вызова соответствующих функций в зависимости от этого значения. Аргументы для функций формируются из других параметров запроса, причем для различных значений 'cmd' (т.е. для разных функций) могут использоваться различные параметры. Нетрудно видеть, что такой исходный код хорошо поддается автоматизированному анализу с помощью приемов, описанных ранее. В 170
соответствии с изложенным выше подходом для генерации тестов для LSB Навигатора был создан инструмент, разбирающий исходный код скриптов, анализирующий используемые параметры запроса, их возможные значения и взаимосвязи. Процесс работы анализатора кода построен следующим образом: определение параметров запроса, используемых в инициализационной части;
определение списка значений параметра 'cmd', встречающихся в инструкциях 'case';
определение имен и возможных значений параметров запроса, используемых в исходном коде внутри каждого блока 'case'; при этом производится рекурсивный анализ вложенных конструкции 'if' и 'switch' внутри каждой ветви.
таким же именем, но получаемый методом GET. Это позволяет гарантировать, что при описанном изменении метода передачи параметров коллизий не возникает и функциональность приложения не нарушается. В процессе генерации перебираются имена параметров, извлеченные из текста скрипта. Для каждого параметра перебираются значения из множества, формируемого следующим образом: если значение параметра сравнивалось в тексте скрипта с некоторой константой, то такая константа заносится во множество возможных значений параметра. Так, для параметра 'cmd' перебираются все значения, найденные в соответствующих предложениях 'case';
Заметим, что анализ вложенных конструкций 'switch' может быть применен ко всему скрипту целиком, начиная с предложения 'switch( $_REQUEST['cmd'] )', и явного выделения такого предложения не требуется. Однако в тестах для LSB Навигатора использование $_REQUEST['cmd'] рассматривается отдельно — в данном случае стоит цель проверить реакцию приложения на все возможные значения параметра 'cmd', в то время как для остальных параметров часть значений может быть либо опущена, либо использована только с определенными значениями других параметров. Вызываемые функции function1, function2, ..., default_function определены в отдельных файлах, к которым нельзя получить доступ через Web-браузер, и которые не содержат обращений непосредственно к массивам $_REQUEST, $_POST или $_GET. В настоящее время тела вызываемых функций не анализируются, хотя потенциально функции могу вести себя по-разному в зависимости от значений своих аргументов, и такой анализ мог бы способствовать повышению качества тестов.
5.2. Генерация тестов На основе сведений, собранных в процессе анализа скрипта, производится генерация тестов для этого скрипта. Тест представляет собой ссылку вида
http://test.host.name/script_name?param1=value1&pa ram2=value2... В настоящее время получение страниц, соответствующих сгенерированным ссылкам, возлагается на внешнюю программу (утилиту wget). При этом все параметры помещаются непосредственно в ссылку, т.е. фактически являются параметрами типа GET. Поскольку реально приложение может ожидать получения некоторых параметров только методом POST, то на время тестирования версии приложения обращения к массиву $_POST заменяются обращениями к массиву $_REQUEST. При этом если LSB Навигатор ожидает получить некоторый параметр методом POST, то он не использует параметр с 171
если имя параметра совпадает с именем поля в одной из таблиц спецификационной базы данных LSB, то во множество потенциальных значений заносятся следующие значения: o
значение, которое содержится в соответствующем поле хотя бы одной записи базы данных;
o
значение-строка, которое гарантированно не встречается ни в одной записи базы данных;
o
значение-число, которое гарантированно не встречается ни в одной записи базы; такое значение заносится в список только для полей БД числового типа. Для определения такого значения из существующих значений выбирается максимальное и к нему прибавляется единица. При этом производится проверка, что полученное число умещается в диапазон значений поля; если это не так, будет выдано предупреждение (однако автоматический выбор нового значения в настоящее время не реализован, поскольку в реальной базе данных таких ситуаций пока не возникает). Стоит отметить, что правила именования полей в базе данных LSB позволяют отличить поля, имеющие числовой тип, от полей текстового типа, на основе самого имени;
есть несколько параметров, для которых все возможные значения задаются вручную.
Поскольку количество скриптов и их параметров велико, то перебор всех возможных сочетаний значений параметров нецелесообразен. В целях уменьшения количества тестов применяются различные стратегии перебора комбинаций параметров — на данный момент генератор может быть настроен на использование каждого второго из всех возможных сочетаний, каждого третьего, и т.д. Недостатком на данный момент является то, что одна и та же стратегия перебора применяется ко всем комбинациям параметров; если для каких-то параметров необходимо применить другую стратегию, то 172
необходима ручная правка генератора тестов. В будущем планируется сделать генератор более удобным в настройке.
5.3. Вынесение вердикта Вся содержательная часть страниц, генерируемых LSB Навигатором, формируется на основе спецификационной базы данных LSB. Поэтому одним из основных критериев успешности выполнения скриптов, образующих Навигатор, является успешность взаимодействия с базой данных. В качестве СУБД в Linux Foundation используется MySQL, для работы с ней из Навигатора используются соответствующие функции PHP. В соответствии с описанным ранее подходом, непосредственно из скриптов вызываются функции-обертки, производящие обращение к реальным функциям и первичную обработку возвращаемых результатов. Так, при получении ошибки от MySQL функции-обертки выводят на генерируемую страницу фразу 'MySQL ERROR', за которой следует непосредственно текст ошибки. Эти функция — единственный потенциальный источник фразы 'MySQL ERROR' на странице; при нормальном функционировании приложения она не может возникнуть, поэтому ее появление в тексте свидетельствует об ошибке. Каждая получаемая в процессе тестирования страница проверяется на соответствие стандарту XHTML 1.0. Для проверки соответствия страницы стандарту XHTML используется Offline HTMLHelp.com Validator [13]. Также после выполнения производится анализ логов HTTP-сервера (в случае LSB Навигатора — Apache), из которого выбираются предупреждения и ошибки, выдаваемые интерпретатором PHP. В будущем планируется расширить анализ текста страниц HTML, генерируемых приложением в процессе тестирования. Например, на данный момент встречаются ошибки, связанные с некорректным формированием ссылок из страницы на другие части документа. В случае LSB Навигатора можно утверждать, что если ссылка содержит обращение к скрипту, передавая ему некоторые параметры, но при этом часть параметров имеет пустое значение, то такая ссылка некорректна — перейдя по ней, пользователь получит сообщение о некорректном значении параметров. Поиск таких ссылок в документе является достаточно простой задачей. Кроме того, интересной представляется возможность более глубокого анализа содержимого страниц на основе запроса, в ответ на который они были получены. В общем случае для такой проверки понадобится создание программы, фактически дублирующей функциональность приложения. Однако вполне вероятно, что можно выявить зависимости между параметрами запроса и некоторыми элементами генерируемой страницы, которые позволят не только автоматически создавать тестовые запросы, но и вычислять, какие элементы должны обязательно присутствовать на полученной в результате запроса странице. 173
5.4. Результаты Автоматические тесты для LSB Навигатора позволили к настоящему времени выявить около 30 ошибок, связанных с функциональностью. При этом общее число ошибок, занесенных в базу ошибок Навигатора за время его разработки — 250. Кроме того, автоматические тесты играют роль регрессионных и позволяют следить за тем, что генерируемые приложением страницы соответствуют стандарту XHTML; выполнение этой работы вручную нереалистично из-за сложности приложения и его базы данных. Разработка же анализатора скриптов и генератора тестов заняла 2 человеко-дня. Характеристики генерируемых тестовых наборов по состоянию на декабрь 2007 года приведены в Таблице 1. Тесты делятся на две категории, в соответствии с режимом работы LSB Навигатора, для которого они предназначены. LSB Навигатор предоставляет два режима работы — режим пользователя (“Browse mode”) и режим администратора (“Administration mode”). При работе в режиме пользователя не допускается никаких изменений в базе данных (т.е. осуществляются только выборки данных при помощи оператора SELECT), в то время как в режиме администратора предоставляется практически полный контроль над базой данных. Скрипты, реализующие режим администратора, могут осуществлять гораздо больше различных запросов к базе данных и принимают на вход значительно большее число параметров. Ввиду этого тестирование работы в режиме администратора занимает больше времени, чем в случае режима пользователя. Режим работы Пользователь Администратор
Количество тестов 2912 28740
Время генерации 15 с 10 мин.
Время тестирования 1ч. 30 мин. 6 ч. 15 мин.
Таблица 1. Характеристики тестовых наборов для режимов пользователя и администратора LSB Навигатора. Число тестов для режима администратора превосходит число тестов для режима пользователя в примерно 10 раз, а время их генерации — в 40. Это обусловлено тем, что при генерации тестов для режима администратора анализатор кода использует существенно больше системных ресурсов, что, в частности, приводит к активному использованию swap-раздела. В то же время затраты на выполнение тестов для этого режима отличаются не столь сильно; этот факт объясняется тем, что любой запрос, выполняемый в режиме администратора, затрагивает только одну таблицу базы данных (более того, существенная часть запросов обращается только к одной записи таблицы). В режиме же пользователя выполняется много сложных агрегатных запросов, выбирающих данные сразу из нескольких таблиц (например, при сборе различных статистических данных). 174
Большую часть времени (порядка 80%) занимает проверка получаемых страниц на соответствие спецификации XHTML. Соответственно, отключение этой проверки позволяет за то же время сгенерировать и выполнить большее количество тестов (например, перебирая больше различных комбинаций параметров). Более того, время анализа результатов тестирования сильно зависит от числа выявляемых при такой проверке ошибок (что обусловлено особенностями используемого для проверки инструмента) — так, наличие ошибок, проявляющихся в 250 тестах для режима пользователя (8% от общего количества) привело к увеличению времени их выполнения до 8 часов. Что касается непосредственно ошибок, выявляемых тестами, то их можно разделить на следующие категории. Вывод имен классов, типов и шаблонов без преобразования угловых скобок, знака ‘&’ и других специальных символов (на страницах, содержащих декларации функций и классов C++), которые могут быть интерпретируемых Web-браузером как часть разметки страницы, а не как данные. Такие ошибки выявляются в процессе проверки страницы на соответствие спецификации XHTML.
Ошибки, связанные с экранированием символов в строках, используемых в запросах к базам данных. Отсутствие экранирования приводит к некорректности запроса SQL и получению ошибки при работе с СУБД. Обнаружение таких ошибок чрезвычайно важно, т.к. их наличие в приложении делает потенциально возможным использование SQL-инъекций — способа взлома сайтов и программ, работающих с базами данных, основанного на внедрении в запрос произвольного SQL-кода [16].
Ошибки, связанные с некорректной обработкой переданных параметров. При определенных значениях параметров на их основе может быть создан некорректный SQL-запрос, что приведет к ошибке взаимодействия с СУБД.
Обращение к ассоциативному массиву по некорректному ключу. Такие ошибки выявляются на основе анализа лог-файлов HTTPсервера. Как правило, они являются следствием некорректного задания имен полей, выбираемых в запросе к базе данных, либо банальных опечаток.
Измерение покрытия кода показало, что в процессе тестирования покрывается 60% строк кода скриптов (как уже упоминалось ранее, всего скрипты содержат около 17000 строк кода). Для сравнения — если просто обратиться к каждому скрипту без передачи ему каких-либо параметров, то покрытие составит 15%. Для измерения покрытия используется свободный инструмент PHPCoverage компании Spike Source [17]. Стоит отметить, что анализ 175
покрытия кода также помогает выявлять недостижимый код, удаление которого приводит к увеличению покрытия. Несмотря на достаточно высокое значение тестового покрытия, в процессе ручного тестирования встречаются ошибки, которые потенциально могли быть выявлены и автоматическими тестами (как правило, это ошибки работы с базой данных, либо ошибки формирования страницы, приводящие к нарушению спецификации XHTML). Такие ошибки проявляются на специфических значениях параметров при определенном их сочетании; они не выявляются автоматически, поскольку в процессе генерации тестов перебираются не все сочетания значений параметров. Как уже отмечалось ранее, генератор тестов обладает некоторыми средствами настройки стратегий перебора параметров, эти средства осуществляют достаточно грубую настройку. В результате при задании достаточно жестких ограничений на комбинации параметров тесты пропускают некоторые ошибки, а даже незначительное ослабление этих ограничений приводит к слишком большому количеству тестов, выполнение которых может длиться больше недели. Поэтому возможность более тонкой настройки генератора является одним из приоритетных направлений развития в ближайшем будущем; также возможно, что уменьшению числа тестов без потери качества может помочь более глубокий анализ зависимостей между параметрами. Тем не менее, в автоматических тестах предусмотрена возможность использования запросов, созданных человеком, наряду со сгенерированными тестовыми запросами. Все запросы, которые привели к ошибкам, но при этом не попали в список сгенерированных, добавляются в «ручной» список. Таким образом, можно гарантировать, что если по каким-то причинам ошибка появится снова, то она будет выявлена автоматически.
6. Заключение Роль человека в тестировании приложений с графическим пользовательским интерфейсом, к которым относятся и Web-приложения, переоценить трудно; проверка многих аспектов, касающихся интерфейса, с трудом поддается автоматизации. Однако в случае Web-приложений ситуация несколько упрощается, поскольку для многих из них большую часть того, что видит пользователь на экране, можно получить автоматически и в текстовой форме — в виде документа HTML. Многие современные инструменты используют этот факт, предоставляя тестировщикам возможность проверки свойств различных элементов страницы, отображаемой Web-браузером, сводя такое тестирование к проверке соответствующих элементов HTML-кода страницы. В то же время большинство современных средств тестирования Webприложений используют в своей работе сценарии, создаваемые людьми; полноценное тестирование больших приложений с применением такого подхода требует больших людских ресурсов. Также ресурсоемким оказывается создание и поддержание в актуальном состоянии набора 176
регрессионных тестов. Причем для приложений, у которых содержательная часть одной и той же страницы постоянно меняется, возможность создания сложных тестовых сценариев сомнительна - ведь страницы, генерируемые при создании сценария, не могут быть использованы в качестве эталонов при вынесении вердикта об успешности прохождения теста. В таких случаях приходится применять менее строгие проверки, чем сравнение страниц с эталоном целиком. Однако создание тестов и выполнение ряда несложных проверок может быть автоматизировано, что позволяет существенно экономить ручной труд. В то же время даже достаточно простые проверки могут помочь выявить серьезные ошибки. Одним из методов автоматизации тестирования Web-приложений является генерация тестов на основе анализа исходного кода приложения, возможные подходы к которой изложены в данной статье. Для выявления ошибок в генерируемых приложением страницах HTML может быть использован ряд автоматических проверок, которые либо вовсе не зависят от кода приложения, либо требуют добавления в код достаточно несложных конструкций, не влияющих на функциональность. Предлагаемый подход к генерации тестов на основе исходного кода приложения гораздо больше зависит от структуры кода. Впрочем, эта структура не налагает ограничений на саму возможность применения метода; однако именно от нее зависит сложность реализации метода для конкретного приложения. Соответствие кода некоторым условиям может существенно упростить создание тестов и в тоже время сделать их более качественными. Конкретные формулировки таких условий зависят от используемого языка программирования (более точно — от способа доступа к параметрам, передаваемым приложению, который предоставляет язык); в статье приводятся формулировки для приложений, написанных на PHP. Такие условия могут быть учтены при создании приложения. При их соблюдении для анализа кода не требуется полноценного парсера используемого языка программирования; достаточным оказывается создание существенно более простого инструмента. Примером приложения, для которого предлагаемый подход позволяет получать достаточно качественные тестовые наборы за короткое время, является LSB Навигатор (распространяющийся по лицензии GPL; исходный код может быть получен в соответствующем разделе Linux Foundation Bazaar [18]). Естественно, реализация метода зависит от языка программирования, на котором написано приложение. Однако для многих скриптовых языков, используемых при создании Web-приложений, сложность реализации этого подхода примерно одинакова; ключевым фактором все-таки является структура исходного кода.
[2] Википедия — свободная энциклопедия. Скриптовый язык. http://ru.wikipedia.org/wiki/Скрипт. [3] IBM Rational Robot. http://www-306.ibm.com/software/awdtools/tester/robot/. [4] HP WinRunner Software. https://h10078.www1.hp.com/cda/hpms/display/main/hpms_content.jsp?zn=bto&cp=111-127-24%5E1074_4000_100__. [5] Empirix e-TEST suite. http://empirix.com/products-services/w-testing.asp. [6] PureTest — Users Guide. Minq Software, 2006. http://minq.se/products/pureload/doc/html/puretest/usersguide/index.html [7] eValid Web Testing & Analysis Suite. http://www.soft.com/eValid/. [8] Web Application Testing with Puffin. http://www.ibm.com/developerworks/opensource/library/os-puffin.html. [9] С. Рогов, Д. Намиот. Тестирование производительности Web-серверов. Открытые системы, 12:55–64, 2002. [10] А. А. Сортов, А. В. Хорошилов. Функциональное тестирование Web-приложений на основе технологии UniTESK. Труды ИСП РАН, 8(1):77–97. [11] RFC 2616. Hypertext Transfer Protocol — HTTP/1.1. [12] Jmeter User's Manual. Jakarta Project, 2007. http://jakarta.apache.org/jmeter/usermanual/index.html. [13] Offline HTMLHelp.com Validator. http://www.htmlhelp.com/tools/validator/offline/index.html.en. [14] LSB Navigator. http://linux-foundation.org/navigator. [15] LSB Specification Database. http://ispras.linux-foundation.org/index.php/LSB_Database_Home. [16] Википедия — свободная энциклопедия. Инъекция SQL. http://ru.wikipedia.org/wiki/Инъекция_SQL. [17] Spike PHPCoverage. http://developer.spikesource.com/wiki/index.php/Projects:phpcoverage. [18] Linux Foundation Bazaar — DB Navigator module. http://bzr.linux-foundation.org/lsb/devel/dbadmin.
Литература [1] XHTML 1.0. The Extensible HyperText Markup Language (Second Edition). http://www.w3.org/TR/xhtml1/.
177
178