Автоматическая генерация позитивных и негативных тестов для тестирования фазы синтаксического анализа
С.В. Зеленое, С.А. Зеленоеа
Аннотация. В статье описывается методика автоматической генерации наборов позитивных и негативных тестов для тестирования фазы синтаксического анализа. Предлагаются критерии покрытия для таких наборов, основанные на модельном подходе к тестированию, и методы генерации наборов тестов, удовлетворяющих предложенным критериям. Также приводятся результаты практического применения описанной методики к тестированию синтаксических анализаторов различных языков, в том числе языков С и Java.
1. Введение
Компилятор является инструментом, требования к надежности которого чрезвычайно высоки. И это неудивительно, ведь от правильности работы компилятора зависит правильность работы всех скомпилированных им программ. Из-за сложности входных данных и преобразований задача тестирования компиляторов является весьма трудоемкой и непростой. Поэтому вопрос автоматизации всех фаз тестирования (создания тестов, их прогона, оценки полученных результатов) стоит здесь особенно остро.
Синтаксический анализ является частью функциональности любого компилятора. От корректности синтаксического анализа зависит корректность практически всей остальной функциональности - проверки семантических ограничений, оптимизирующих преобразований, генерации кода. Поэтому решение задачи тестирования синтаксических анализаторов является базой для решения задач тестирования всех остальных компонент компилятора.
Для очень многих языков программирования существует формальное описание синтаксиса - описание грамматики языка в форме BNF, а для тех языков, для которых существуют только эталонные компиляторы (например, COBOL), делаются активные попытки построить такое описание (см. [9, 10, 14]). BNF языка является одновременно и спецификацией функциональности синтаксического анализа, таким образом, в этой области наиболее привлекательным является тестирование на основе спецификаций (см. [17]). Существование формального описания позволяет автоматизировать процесс
построения тестов, что существенно снижает трудозатраты, а систематичность тестирования повышает доверие к его результатам.
Построением тестов по грамматике занимались многие авторы. Основополагающей работой в этой области является работа [18], в которой сформулирован следующий критерий покрытия для множества позитивных тестов: для каждого правила в данной грамматике в множестве тестов должно присутствовать предложение языка, в выводе которого используется это правило. В той же работе Пардом предложил метод построения минимального тестового набора, удовлетворяющего этому критерию. Однако указанный критерий оказался недостаточным. Ламмель в работе [9] показал, что тестовые наборы, построенные алгоритмом Пардома, не обнаруживают простейших ошибок. Ламмель также предложил более сильный критерий покрытия, состоящий в том, что покрывается каждая пара правил, одно из которых можно применить непосредственно после другого.
Предлагаемые другими авторами методы являются вероятностными (см. [7, 13, 11, 12]) и не описывают критериев покрытия, и потому для них возникает вопрос остановки генерации тестов, который решается, например, с помощью введения вероятностей появления правил и уменьшения этих вероятностей при каяедом новом появлении правила в выводе. В любом случае завершение работы алгоритма за конечное время является проблемой. Кроме того, произвольность остановки генерации нарушает систематичность тестирования.
Все приведенные выше работы касаются генерации позитивных тестов для синтаксического анализатора (т.е. тестов, являющихся предложениями целевого языка). В настоящее время работы, предлагающие методы генерации негативных тестов для синтаксических анализаторов (т.е. тестов, не принадлежащих целевому языку), практически отсутствуют. Однако такие тесты также важны, поскольку пропуск неверной последовательности лексем на этапе синтаксического анализа может привести к аварийному завершению компиляции.
В работе [8] высказано предположение, что если имеется генератор предложений языка из грамматики (генератор позитивных тестов для синтаксического анализатора), то для генерации негативных тестов для синтаксического анализатора можно использовать метод мутационного тестирования (mutation testing)1 (см. [6, 15]). Идея состоит в том, что в
1 В общих словах мутационное тестирование состоит в следующем. Из тестируемого компонента получают множество его модификаций (мутантов), каждая из которых содержит ровно одну ошибку. Говорят, что мутант убит, если на некоторых входных данных его выход отличен от выхода исходной программы. Если имеется множество тестовых входных данных для программы, то с помощью мутационного анализа (mutation analysis) можно оценить качество этих тестов. Именно, если имеется множество мутантов, то критерий покрытия говорит, что все мутанты должны быть убиты. В случае синтаксического анализатора логично в качестве мутируемого материала рассматривать грамматику языка, т.к. ошибочный анализатор фактически распознает другой язык. В этом случае грамматика-мутант будет убита, если найдется последовательность токенов, принадлежащая языку, задаваемому этой грамматикой-мутантом и не принадлежащая
(продолжение на следующей стр.)
исходную грамматику вносятся изменения (мутации) для получения
грамматик, описывающих близкие, но не эквивалентные исходному языки. Эти мутированные грамматики подаются на вход генератору тестов для получения потенциально негативных тестов. Общие проблемы данного подхода состоят в следующем:
• Г рамматика-мутант может оказаться эквивалентной исходной
грамматике. Такие мутанты должны быть выявлены и не должны
использоваться для генерации тестов.
• Даже если грамматика-мутант не эквивалентна исходной, полученные
из нее тесты могут оказаться правильными. Выявить эти тесты можно лишь прогнав их через эталонный синтаксический анализатор,
которого может и не быть (например, в случае создания нового или расширения существующего языка).
В настоящей работе описаны критерии покрытия, нацеленные на алгоритмы синтаксического анализа. Такой подход представляется оправданным, поскольку тестовые наборы строятся для тестирования синтаксических анализаторов, и эффективность тестового набора должна оцениваться исходя из характеристик, относящихся к тестируемым компонентам (т.е. синтаксическим анализаторам), таких как, например, покрытие функциональности или кода. Данная методика разработана в рамках общего модельного подхода к тестированию компиляторов (см. [2,3,4]). Мы рассматриваем известные алгоритмы синтаксического анализа в качестве алгоритмов, моделирующих поведение синтаксического анализатора. Как уже говорилось, в литературе практически отсутствуют работы, посвященные генерации негативных тестов. Настоящая работа призвана закрыть этот пробел.
Статья состоит из введения и трех разделов. В первом разделе содержатся сведения из теории алгоритмов синтаксического анализа. Второй раздел посвящен описанию предлагаемой методики. В нем вводятся понятия позитивных и негативных тестов, описываются критерии покрытия для тестовых наборов, опирающиеся на алгоритмы синтаксического анализа, а также приводятся алгоритмы построения наборов, удовлетворяющих этим критериям покрытия. В третьем разделе описаны результаты практического применения методики.
2. Предварительные сведения
В этом разделе мы приводим некоторые сведения из теории синтаксического анализа. Более подробное изложение приведенных фактов можно найти в известной книге А. Ахо, Р. Сети и Д. Ульмана (см. [5]).
исходному языку (в терминах анализаторов это как раз будет означать, что анализатор-мутант распознал данное предложение, а исходный анализатор - нет, т.е. анализатор-мутант оказался убитым).
Грамматика формального языка задается четверкой О = (Т,1Ч Р, .V). где
• Т - множество терминальных символов или токенов;
• N - множество нетерминальных символов;
• Р - список правил грамматики;
• £ — стартовый символ грамматики.
Множество предложений формального языка, задаваемого грамматикой О,
будем обозначать
Дадим несколько определений.
Расширением грамматики О (или просто расширенной грамматикой) называется грамматика О' = (Т.Ы'.Р1,^), где .V - новый нетерминальный символ, а к множеству правил добавлено правило —> &
Сентенциальной формой будем называть последовательность грамматических символов - нетерминалов и токенов. Далее, греческими буквами из начала алфавита (а, Д у, с>, ...) мы будем обозначать какие-либо сентенциальные формы. Пустую сентенциальную форму будем обозначать через е. Правосентенциальной формой называется сентенциальная форма, для которой существует правый вывод из стартового правила.
Пример. Рассмотрим следующую грамматику:
Б^АВ А —>■ сс!
В —> еС/
С —>Ае
В ней сентенциальная форма сс!В не имеет правого вывода, т.е. не может быть получена с помощью последовательного раскрытия самых правых нетерминалов. Примером правосентенциальной формы может служить форма
АеС/ ►
Основой правосентенциальной формы называется подпоследовательность ее символов, которая может быть свернута в некоторый нетерминал, такая, что сентенциальная форма, полученная из исходной после свертки, может быть свернута в стартовый символ.
Пример. Пусть грамматика та же, что и в предыдущем примере. Форма АеС/ как мы уже заметили, является правосентенциальной. В ней есть две подпоследовательности символов Ае и е(/. которые могут быть свернуты в нетерминалы С и В соответственно. Однако основой является только последовательность еС/ так как сентенциальная форма СС/ невыводима из стартового символа. ►
Активным префиксом правосентенциальной формы называется префикс, не выходящий за границы самой правой основы этой формы.
Пунктом грамматики О называется правило вывода с точкой в некоторой позиции правой части. Множество всех пунктов грамматики О будем обозначать через Ф
Пример. Правило вывода А —> ХУ2 дает 4 пункта: А —> »ЛТ?, А —> А'* 72, .! > Л) •/ и .! >А)/*. ►
Базисным называется пункт с точкой не у левого края правой части правила, а также пункт £> —> •>*> в расширенной грамматике.
Замыканием множества пунктов / (обозначается с1оь'иге(Г)) называется наименьшее множество пунктов, содержащее / в качестве подмножества такое,
что для всякого пункта А —>■ а*Вр Е / и любого правила В —>у пункт В —> лежит В с/0А'ИГе(Г).
Для пары (/,Л). где / некоторое множество пунктов грамматики О, а Л - символ грамматики (терминал или нетерминал), определим функцию goto(l,X) -
замыкание множества всех пунктов^ —>■ аЛ’*/? таких, что. I —>■ ««Л/У е I. Рассмотрим расширенную грамматику О' = (ТЫ'.Р',^), где N1' = Ыи {Л"|. Р = Ри ¡.V —> Л';. Пусть /0 = с1о.\иге( ¡.V —► *Л';). Начиная с /0, строится система
множеств пунктов /0...../;, так, что для всякой пары (Д.Л'). где к = 0.N и Л' -
символ грамматики, существует индекс / = 0.....N такой, что goto(h,X) = /,. Эта
система пунктов называется канонической системой множеств пунктов.
Используя каноническую систему /0...../•,. можно построить конечный автомат
Г’ распознающий активные префиксы, если в качестве состояний л;, взять канонические множества/;, а переходы задать с помощью функции "о/о.
Широко известны два класса алгоритмов синтаксического анализа: ЬЬ-анализ и ЬЯ-анализ (см. [5]). ЬЬ-анализатор с помощью диаграммы переходов или таблицы разбора строит левый вывод предложения целевого языка. Нерекурсивная реализация ЬЬ-анализатора использует стек и таблицу разбора. Изначально в стеке находится символ конца строки $ и стартовый символ грамматики. На каждом шаге рассматривается символ на вершине стека А' и текущий входной символ а. Действия анализатора определяются этими двумя символами:
• если А' = а = $, то анализатор прекращает работу и сообщает об успешном завершении разбора;
• если X = аф $, анализатор удаляет из стека символ А' и переходит к следующему символу входного потока;
• если А' является нетерминалом, анализатор ищет такую альтернативу раскрытия символа А' для которой символ а является допустимым первым символом. После того, как требуемая альтернатива найдена, символ А' в стеке заменяется обратной последовательностью символов альтернативы. Например, если искомая альтернатива А' —>■ АВС, то анализатор заменит А' на вершине стека на последовательность СВА, т.е. на вершине стека
окажется символ А. Конфликты, возникающие в процессе поиска альтернатив, могут разрешаться, например, с помощью “заглядывания вперед”, т.е. просмотра нескольких входных символов вместо одного.
Анализатор завершает работу, когда на вершине стека оказывается символ конца строки $.
Рассмотрим теперь 1Л-анализатор, построенный на основе стека. У такого ЬЯ-анализатора имеются две основные операции:
• перенос символа из входного потока в стек;
• свертка нескольких последовательных символов на вершине стека в некоторый нетерминал.
Работа анализатора происходит так, что в стеке все время находится активный префикс некоторой правосентенциальной формы. При переносе символа и свертке на вершину стека кладется символ состояния ь) конечного автомата V, кодирующий текущий активный префикс. 1Л-анализатор принимает решение о переносе ИЛИ свертке, ИСХОДЯ ИЗ пары (СИМВОЛ 5), текущий токен входного потока). Анализатор завершает работу, когда в стеке оказывается стартовый символ грамматики.
3. Описание методики
3.1. Позитивные и негативные тесты для синтаксического анализатора
В данной работе парсером мы называем булевскую функцию, заданную на множестве последовательностей токенов и принимающую значение “истина”, если последовательность является предложением данного формального языка, и “ложь” - иначе. Конечно, реальные парсеры могут иметь дополнительную функциональность (например, помимо булевского значения выдавать дерево разбора или идентификацию ошибки), но здесь мы такую функциональность не рассматриваем.
Позитивный тест для парсера - это последовательность токенов, на которой парсер выдает вердикт “истина”, т.е. последовательность токенов, являющаяся предложением целевого языка.
Негативный тест для парсера - это последовательность токенов, на которой парсер выдает вердикт “ложь”, т.е. последовательность токенов, не являющаяся предложением целевого языка.
Для построения какого-нибудь позитивного теста достаточно, следуя правилам грамматики и ограничив рекурсию правил, вывести из стартового символа некоторую сентенциальную форму, состоящую из одних токенов. При построении же негативных тестов возникают два вопроса: насколько произвольна должна быть соответствующая последовательность токенов, и как добиться того, чтобы она действительно не принадлежала целевому языку.
Сначала ответим на второй вопрос: как получить последовательность токенов, гарантированно не принадлежащую множеству предложений целевого языка.
Рассмотрим грамматику О = (Т ,Ы,Р,5). Для каждого грамматического символа Л' еТиЫ, определим множество Цу вхождений символа Л' в грамматику О. Это множество состоит из всех пар
(правило р ЕР, номер У символа в правиле р) таких, что символ, стоящий на У-ом месте в правой части правила р является грамматическим символом Л'. Пару (/?,/) ЕЦу будем называть вхождением символа X в правило р.
Пусть У - токен. Для каждого вхождения и Е1Л, и = (/?,/). р =Л'—> аф токена У в
грамматику О можно построить множество р, токенов У'еТ таких, что существует вьшод
Здесь греческие буквы обозначают некоторые субсентенциальные формы, т.е. последовательности нетерминалов и токенов. Если в грамматике О существует
* р
вывод Б=^-уХ=^уа1 предложения, оканчивающегося токеном У, то будем считать, что множество Р„ содержит пустую последовательность е Е р,.
Через Рг будем обозначать объединение множеств р, для токена У:
я - и
чяРс
Иными словами, множество Рг - это множество токенов, каждый из которых допустим для токена У в качестве следующего.
В дальнейшем нас главным образом будет интересовать дополнение к множеству Рг в множестве Т и{е}. Будем обозначать это дополнение через
-(Ги{д})\ П-
Теорема 1. Последовательность токенов, содержащая подпоследователь-ность
УУ', где У'е ОТ,, не является предложением языка, описываемого грамматикой О.
Доказательство. Очевидно из построения множества ►
Для последовательности токенов а = Уь..У„ такой, что существует вывод 8=^-(1ау.
можно определить множество токенов ^Т^такое, что, если У'Е Фц. то не
существует вывода 8=?/За?у. Тогда любая последовательность /?аУ'у, где У'Е 7[л, не является предложением языка, описываемого грамматикой О.
Итак, мы научились получать последовательности токенов, заведомо не являющиеся предложениями целевого языка. К вопросу о произвольности негативной последовательности токенов мы вернемся в следующем параграфе.
3.2. Критерии покрытия
Как видно из описания LL- и LR-анализаторов, основной момент их работы -принятие решения о дальнейших действиях на основании некоторых неполных данных (прочитанной части входного потока). Для LL-анализатора ситуации выбора соответствует пара (нетерминал на вершине стека, текущий входной символ), а для LR-анализатора - пара (символ состояния конечного автомата на вершине стека, текущий входной символ). Отсюда возникают следующие критерии покрытия для позитивных тестовых наборов:
(PLL) Покрытие всех пар
(нетерминал^, допустимый следующий токен I). где пара (A,t) считается покрытой тогда и только тогда, когда в тестовом наборе существует последовательность токенов, являющаяся предложением целевого языка, имеющая вывод S=*aAp =>atyp. Иными словами, LL-анализатор, обрабатывая эту последовательность, получит ситуацию, когда на вершине стека будет находиться символ А, а текущим входным символом будет токен t. Модификация этого критерия для расширенной формы BNF грамматики была сформулирована в работе [1].
(PLR) Покрытие всех пар
(символ Sj состояния конечного автомата, помеченный символом А'переход ИЗ СОСТОЯНИЯ .V,). где пара (.v„A) считается покрытой тогда и только тогда, когда в тестовом наборе существует предложение языка, имеющее вывод
*
S=t-a\'/i такой, что префикс а отвечает состоянию .v,. Или, что то же самое, LR-анализатор, обрабатывая это предложение получит ситуацию, когда на вершине стека будет находиться символ .v,. а началом текущего входного потока будет последовательность токенов, отвечающая символу А'.
Аналогично возникают следующие критерии покрытия и для негативных тестовых наборов (эти критерии имеют параметр г - количество “правильных” токенов, предшествующих “неправильному” токену):
(NLLr) Пусть А - нетерминал. Последовательность токенов th..tr назовем допустимой для А предпоследователъностъю токенов, если существует сентенциальная форма (/.и...!,, \[1. выводимая из стартового правила. Рассмотрим объединение множеств 01, , по всем допустимым для А предпоследовательностям токенов длины г < R. Критерий состоит в том, что все пары (. I,/'). где /' из рассмотренного объединения, должны быть покрыты. Здесь покрытие пары (. I,/') означает, что среди тестов имеется последовательность токенов, не принадлежащая целевому языку, такая, что LL-анализатор, обрабатывая эту последовательность.
получит ситуацию, когда на вершине стека будет находиться символ А, а текущим входным символом будет “некорректный” символ Г'.
(ЖЛя) Пусть .V, - символ состояния конечного автомата, определяющего активные префиксы. Последовательность токенов и...и назовем допустимой для 5, предпоследователъностъю токенов, если существует выводимая из стартового правила последовательность токенов такая, что ее префикс а[,...[г отвечает состоянию
Рассмотрим объединение множеств , по всем допустимым для .V, предпоследовательностям токенов длины г < К. Критерий состоит в том, что все пары (.\„Г). где Г' из рассмотренного объединения, должны быть покрыты. Здесь покрытие пары (л„;') означает, что среди тестов имеется последовательность токенов, не принадлежащая целевому языку, такая, что ЬЯ-анализатор, обрабатывая эту последовательность получит ситуацию, когда на вершине стека будет находиться символ л„ а текущим входным символом будет Л
Для получения ситуации (. I,/') в критерии (Л/,/,) или ситуации (\„/') в критерии (Ы1Л) требуется, чтобы парсер нормально проработал какое-то время, а затем встретил неверный символ. Для достижения этой цели необходимо, чтобы токены, идущие в последовательности до неверного токена Л образовывали префикс некоторого предложения языка, при разборе которого возникала бы требуемая ситуация в стеке. Поэтому в качестве негативного теста мы будем рассматривать измененное (с помощью вставки или замены токенов) предложение целевого языка так, чтобы в нем содержалась неправильная
последовательность токенов 11...1,1'. где 'ТС, ,..2
Завершая этот параграф, введем еще два полезных критерия покрытия для грамматик специального вида.
Пусть грамматика О такова, что ее каноническая система множеств пунктов удовлетворяет следующему свойству: если /, и /; — два различных множества из канонической системы, то множества базисных пунктов из И и 1] не пересекаются. Заметим, что для такой грамматики покрытие всех пар (состояние конечного автомата, переход из этого состояния в другое)
2 Существует связь между предлагаемым подходом и методом мутационного тестирования. Именно, для любой последовательности токенов, являющейся негативным тестом в описанном выше смысле (т.е. предложением языка, “испорченным” с помощью вставки/замены “нехорошего” токена) можно построить грамматику-мутант такую, что данный негативный тест будет являться предложением языка, описываемого этой грамматикой-мутантом.
Одним из принципов мутационного тестирования является так называемый эффект взаимосвязи (coupling effect): при обнаружении простых ошибок будут обнаруживаться и более сложные (см. [16]). Согласно этому эффекту взаимосвязи, мутации должны быть простыми. Заметим, что предлагаемый подход вполне согласуется с этим принципом.
достигается при покрытии всех пунктов грамматики. Рассмотрим следующий критерий покрытия для наборов позитивных тестов:
(\VPLR) Покрытие всех пар (пункт к = В —> а •.!'/? грамматики О, допустимый первый токен Г для символа А”). Пара (пЛ) считается покрытой тогда и только тогда, когда в тестовом наборе существует предложение
* 4 *
языка, имеющее вывод 8=$-уВд=$-уаХ[5д=&уаЦф8.
Для грамматик указанного типа этот критерий является более сильным, чем критерий РЬК. Действительно, нетрудно показать, что каждое состояние определяется множеством своих базисных пунктов. Отсюда, поскольку для грамматик указанного класса подмножества базисных пунктов у разных состояний не пересекаются, то покрыв все пункты, мы покроем и все состояния.
Аналогично можно сформулировать критерий покрытия для наборов негативных тестов:
(\VNLRr) Пусть п = В —> ««/У — пункт грамматики О. Последовательность токенов и. ,ЛГ назовем предпоследоеателъностъю токенов допустимой для ж, если существует выводимая из стартового правила последовательность токенов ...Г,.•А, имеющая вывод
£ уЗй Л уа 1 ^11 п ^ Л.
т.е. /(/ь.Л,- выводится из уа, а X - из ¡38. Рассмотрим объединение множеств по всем допустимым для ж предпоследовательностям токенов длины г < К. Критерий состоит в том, что все пары (7Т,['). где из рассмотренного объединения, должны быть покрыты. Здесь пок-рытие пары (пЛ') означает, что среди тестов имеется последовате-льность токенов, не принадлежащая целевому языку, такая, что некоторый ее префикс имеет вид ш,.где -некоторая допус-тимая для к предпоследовательность токенов такая,
ЧТО ¿'е ЭТ„...,Г.
3.3. Тестовые наборы
В данном разделе мы опишем способы генерации тестовых наборов, удовлетворяющих некоторым из описанных выше критериев покрытия.
3.3.1. Позитивные тесты
Пусть В - нетерминал, А - произвольный грамматический символ. Множество всех вхождений (р,/) символа А в грамматику, таких что р = В —> «. I/У. обозначим через и/. Упорядочим и перенумеруем элементы множества и/, /-й элемент этого множества обозначим через. \ .
Пример. Если нетерминал В определяется тремя правилами
В АНЕ & ■+ СЛРЛ В ■* КВ,
то для символам! имеем три вхождения: А{В,1) из первого правила (В —*А\ОЕ), Л{В,2) И А{В,Ъ) из второго правила (В —> СЛ2ОЛ3). В третьем правиле вхождений символам! нет. ►
Введем следующее обозначение: будем писать А < В, если . I входит в правую часть какого-либо правила, определяющего нетерминал В.
Теорема 2. Для любого грамматического символа^, выводимого из стартового
символа, существует цепочка А < В\ <... < В у: < Б, где Л' - стартовый символ грамматики, а Bi - нетерминалы, такая, что все нетерминалы, входящие в эту цепочку, различны.
Доказательство. То, что символ А выводим из стартового символа, означает,
что для символа А существует какая-то цепочка А <В\ < /4 -< Л', Пусть это
минимальная по длине такая цепочка. Пусть теперь в цепочке Bi <...< Вк < 8
нет повторяющихся нетерминалов, а в цепочке Дч /4 -< Л' есть, т.е.
5,4 = 5/ для некоторого / =;.к. Но тогда мы можем заменить цепочку А < В\
<...< Вк < Л' на цепочку А < В\ <...< 5,.2 -< 5/ Вк < Л', меньшую по
длине, что противоречит минимальности первоначальной цепочки. ►
Доказанная теорема показывает, как можно строить наборы тестов, удовлетворяющие критерию (РЬЬ). Действительно, эта теорема дает
следующий способ построения цепочки А < Вх < Вк < .V:
• Для символа А строим множество Л'| нетерминалов, для которых существует определяющее правило, содержащее А в правой части. Если Л'
Е Л'|. то искомая цепочка построена.
• Для каждого элемента В\ множества Л', строим множества NА в
нетерминалов В2фВ\, для которых существует определяющее правило, содержащее В\ в правой части. Если Л' е NА в , то искомая цепочка
построена.
• Для каждого элемента множества NА в /;<. строим множества ЫА щ /;<1 ; нетерминалов В&{В\.....Дн |. для которых существует определяющее правило, содержащее в правой части. Если Л' е NА щ /;<. ^ , то искомая цепочка построена.
Из теоремы 2 следует, что приведенный алгоритм завершит работу построением искомой цепочки. Заметим теперь, что, имея цепочку А < В\ <. ,.
-< Вк < Л', можно получить из нее несколько выводов сентенциальных форм, если конкретизировать соответствующие вхождения. Каждая из этих сентенциальных форм будет иметь вид аАр, где а. р некоторые последовательности грамматических символов.
Определим функцию/1п1(А), возвращающую множество раскрытий символа^:
• Если А - токен, то _/?г5^4) = {.4}.
• Если А - нетерминал, то
Тогда, если А\ - грамматический символ, не имеющий пустого раскрытия, то //Ул7(«) = ('¡. а если . I имеет пустое раскрытие, то
/1п1(а) = СА и/1п1(А2--.А„).
При этом рекурсия при построении /1п1(А) обрывается так: если при построенииУ?Г5^4) необходимо это же множество, то его считают пустым. Теорема 3. Множество /1п1(А) включает раскрытия А со всевозможными первыми токенами для А.
Доказательство. Следует из построения. ►
Имея выведенную из стартового символа сентенциальную форму а. Iр и множество //>л7(. I). мы можем построить множество сентенциальных форм а)ф,
где у £ /¡г.\1(. I). Для каждой такой сентенциальной формы выберем какую-нибудь последовательность токенов, выводимую из этой сентенциальной формы. Множество выбранных последовательностей обозначим через Теорема 4. Объединение множеств ‘X, для всех нетерминалов А является множеством позитивных тестов, удовлетворяющим критерию (РЬЬ).
Доказательство. Следует из построения и из теоремы 3. ►
Здесь же можно указать способ построения набора позитивных тестов, удовлетворяющего критериям \\~PLR и РЬК.
Пусть В - нетерминал из грамматики О, обозначим через Ь/.(/?) множество сентенциальных форм вида аВр, выводимых из стартового правила, в цепочке вывода которых каждое правило встречается не более чем к раз.
Пусть ж = В —> /.*Лн - пункт грамматики О. Положим
• Если а = ^41...^4„ - сентенциальная форма, то пусть
Пусть С,=аВр Е Ьа(5). Для каждой сентенциальной формы а1у/.ф, где
).уа Е [г.\1(тг). выберем какую-нибудь последовательность токенов, выводимую из этой сентенциальной формы. Множество выбранных последовательностей обозначим через ^Гп-С- Очевидно, что всякая последовательность токенов из !£тт.С является предложением целевого языка, а значит - позитивным тестом для парсера.
Теорема 5. Пусть для каждого нетерминала В Е N грамматики О фиксирована сентенциальная форма £в Е Ь/■(/?). Тогда множество позитивных тестов:
и ^*1*1
удовлетворяет критерию {\VPLR): здесь как и раньше, обозначает множество всех пунктов грамматики О.
Доказательство. Следует из теоремы 3. ►
Теорема 6. Начиная с некоторого к, множество позитивных тестов
и и
удовлетворяет критерию (РЬК).
Доказательство. Поскольку при стремлении к к бесконечности множество Ь/■(/?) стремится к множеству всех сентенциальных форм вида аВ[1. то, начиная с некоторого А', все возможные состояния автомата, определяющего активные префиксы, будут покрыты. Так как мы берем объединение по всем пунктам, то будут покрыты и все пары (л„Л'). где я, - символ состояния конечного автомата, Л -символ грамматики. ►
3.3.2. Негативные тесты
Ранее мы уже заметили, что наиболее естественный способ получения негативного теста - мутация некоторого предложения целевого языка, т.е. замена в этом предложении некоторого участка (возможно пустого) на “неправильную” последовательность токенов.
Пусть а - предложение языка, и пусть в нем отмечен токен с порядковым номером У. Мутацией 1-го рода (обозначается тШ\(аЛ)) мы будем называть замену предложения а = на множество последовательностей токенов вида
¿1 где При мутации 1-го рода в предложение вставляются
“неправильные” токены. Мутацией 2-го рода (обозначается будем
называть замену предложения а = ^..Л„ на множество последовательностей
токенов вида ^..Л/^+2..Лт где /’Е 91,. При мутации 2-го рода в предложении один из токенов заменяется на “неправильный” токен. Кроме того, можно определить мутации 1-го и 2-го рода при г = 0. В этом случае принадлежит множеству недопустимых первых символов, т.е.
г' е ту> | ^ ■=
а операции мутации определяются как и ранее: ти^\..Л„,0) = и
ппй2{[\..Л„,0) = ./,,}•
Из теоремы 1 следует, что обе операции мутации дают негативные тесты. Перейдем к описанию способа получения множества предложений с отмеченными токенами, позволяющего строить множества тестов, удовлетворяющие критериям (1ЧЬЬх) и (Х1Л\).
Пусть к = Б —> В\...В,аВц.\...В„ - некоторый пункт грамматики О.
Рассмотрим следующий алгоритм построения множества предложений с отмеченными токенами.
• Пусть Мл - пустое множество.
• Построим сентенциальную форму аЦв-^б', выводимую из стартового символа (как это сделать, обсуждалось выше). Пусть у - непустая последовательность из множества /1п1(Вм...В„). Добавим в Мл все сентенциальные формы вида а)ф, в которых отмечен первый символ последовательности у. Нетрудно видеть, что этот символ является токеном (это следует ИЗ построения множества /?Г^(...)). Если последовательность ДЧ...Д может быть раскрыта в пустую последовательность токенов, то делаем шаг 3, иначе Мл вычислено.
• Для каждого пункта тг' такого, что тт' = Л' —>■ /./>«. добавим в Мл все сентенциальные формы из множества Л4'. При этом рекурсия при построении Мл обрывается так: если при построении Мл необходимо само это множество, его считают пустым.
Рассмотрим грамматику (5, полученную из грамматики О следующим образом. Множества токенов и нетерминалов, а также стартовый символ в грамматиках ($ и О совпадают. Если в грамматике О присутствует правило вывода А —> В\...В„, то в грамматике присутствует правило . I -^>В„...В\, здесь . I -нетерминал, а Д - грамматические символы. Других правил в грамматике (5
Очевидно, что грамматика © задает язык, каждое предложение которого - это некоторое предложение языка, задаваемого грамматикой О, написанное “задом наперед”.
Возьмем пункт ч= I) Н>п...Н>г |*Д.../?| в грамматике (5, соответствующий пункту тт в грамматике О. Вычислим Мц в грамматике (35. Пусть
Л',т = {(Л,...л;, /) | (ЛЛ',, ;) е М-,-}. Каждую сентенциальную форму из множества Ыл можно раскрыть в одно или несколько предложений целевого языка с отмеченным токеном, если как-нибудь раскрыть все входящие в эту сентенциальную форму нетерминалы. Обозначим полученное множество помеченных предложений через ©*. К нему можно применить ранее определенные операции мутации 1-го и 2-го рода.
Теорема 7. Пусть А - нетерминал и ^ - множество пунктов
В —у В\...В,аВц.\...В„ таких, что Вм =. I. Тогда множества негативных тестов
&г = и и í=h^<¿
Л
удовлетворяют критерию (ЫЬЬ\).
Доказательство. Действительно, пусть существует вывод 1/У.
Нетерминал^ получается на некотором шаге этого вывода, т.е.
5 » 7_Р) - В; А V,. ад 4 яь*#,
где р = Б —* В\...В^АВц-2~-В„. По условию, для пункта .”= Б —> В„...В^у4аВ^..В1 из грамматики © было построено множество Мт. Отсюда ясно, что, если в Мц-
существует последовательность вида /Аш. то ситуация (. I,/'). где /'£ 9%, покрыта.
Заметим, что исходный вывод предложения аХАр может быть преобразован в вывод в грамматике (5:
ь № - - ■ - г ■ #1/ » $Ш-
Если последовательность В^..В\ не имеет пустого раскрытия, то существует такая последовательность из//г^Д...^), что Г является ее первым токеном, т.е. в множестве Мтг имеется сентенциальная форма а 1(н. что и требовалось. Случай, когда Д...Д может быть раскрыта в пустую последовательность, разбирается аналогично. ►
Теорема 8. Множества негативных тестов
&г = и и £ = 1,2
Л
определенные в теореме 7, удовлетворяют критерию (}¥РЬК\).
Доказательство. Аналогично доказательству теоремы 7. ►
Пусть ж = Б —у В\...В,аВц.\...В„ - пункт грамматики О. Заметим, что на втором
*
этапе построения Мж мы выбирали сентенциальную форму 8=^-(/,1)[1. так что в действительности построенное Мп, а значит и 0Т. зависят от этой сентенциальной формы. Пусть Ь/(/)) - множество сентенциальных форм вида а!)р. в цепочке вывода которых каждое правило встречается не более к раз. Теорема 9. Начиная с некоторого к, множества негативных тестов
= и и АНЙ (Ы| т = 1’ 2
удовлетворяют критерию (\и(\ ).
Доказательство. Поскольку при стремлении к к бесконечности множество Ь/Д)) стремится к множеству всех сентенциальных форм вида а!)р. то, начиная с некоторого А', все возможные состояния автомата, определяющего активные
префиксы, будут покрыты. Таким образом, будут покрыты и все пары где s, - символ состояния конечного автомата, f - негативный токен. ►
4. Практические результаты.
Для апробации предложенной методики нами были построены следующие генераторы:
• Г енератор GP множества позитивных тестов для парсера,
удовлетворяющий критерию PLL.
• Г енератор Gn множества негативных тестов для парсера,
удовлетворяющий критериям WNLR\ и NLL\.
Оба генератора получают на вход BNF целевого языка. У генератора GP
имеются параметры, позволяющие регулировать количество тестов, а у
генератора GN - параметр, позволяющий указывать способ мутации.
Кроме того, генератор Gy генерирует документированные тесты, т.е. в каждом тесте имеется комментарий о том, в каком месте следует ожидать ошибку и какого рода эта ошибка. Таким образом, если парсер имеет дополнительную функциональность - выдачу сообщения об ошибке при ее обнаружении, то можно протестировать и эту функциональность, если сравнить ожидаемые и полученные данные об ошибке.
Генератором GP были сгенерированы наборы тестов для BNF следующих языков:
• С - расширение ANSI С, для которого реализован известный компилятор gcc (см. [19]);
• трС - расширение ANSI С, разработанное для программирования параллельных вычислений (см. [23]);
• Java (версия 1.4);
• J 'ir/л а - спецификационное расширение языка Java, разработанное в ИСП РАН (см. [21]).
Некоторые данные об использовавшихся грамматиках приведены в таблице:
Характеристики С Java Jwva
Количество нетерминалов 70 135 174
Количество токенов 93 101 140
Минимальная мощность 1 19 34
Максимальная мощность И, 93 101 140
Средняя мощность 'Jt, 68 79 119
Генератором Gn были сгенерированы наборы тестов для BNF трех указанных языков (исключая трС). Количественные характеристики сгенерированных тестовых наборов приводятся в таблицах:
Генератор Количество тестов
с Java J@va
GP 137307 137148 219221
gn 43448 71943 145758
Генератор Время генерации
С Java J@va
Gf 19 m 30 s 19 m 51 s 43 m 08 s
gn 3 m 01 s 7 m 14 s 19 m 25 s
Генератор Средний размер теста в байтах
С Java J@va
Gf 58 68 112
gn 219 220 266
Позитивные тесты для Java и J@va были прогнаны через парсер языка J@va, разработанный в ИСП РАН (см. [21]). Указанный парсер был разработан при помощи JavaCC (см. [20]). В результате прогона тестов было обнаружено восемь ошибок. Позитивные тесты для языка трС обнаружили 12 ошибок в компиляторе этого языка. Позитивные тесты для С были прогнаны на широко используемом компиляторе gcc (см. [19]). При этом на 112 тестах gcc зациклился. Негативные тесты для С были прогнаны на парсере языка С, разрабатываемом в ИСП РАН (см. [22]), и обнаружили две ошибки.
5. Заключение
На основе модельного подхода к тестированию были разработаны критерии покрытия для наборов позитивных и негативных тестов для парсеров.
Одно из достоинств предложенного в статье метода построения негативных тестов для синтаксического анализатора - гарантия негативности построенных тестов. Эго позволяет отказаться от использования каких бы то ни было эталонных синтаксических анализаторов.
Были разработаны методы генерации наборов тестов, удовлетворяющих предложенным критериям. С помощью этих методов были построены генераторы позитивных и негативных тестовых наборов. Сгенерированные для разных языков тестовые наборы были использованы для тестирования ряда парсеров и компиляторов и выявили в тестируемых компонентах ошибки.
Таким образом, практические результаты применения данной методики показывают адекватность предложенных критериев покрытия, а также целесообразность использования предложенной методики для тестирования промышленных компиляторов.
Литература
1. А. В. Демаков, С. В. Зеленов, С. А. Зеленова. Тестирование парсеров текстов на формальных языках // Программные системы и инструменты (тематический сборник факультетаВМиЕСМГУ). -2001. -№2. -р. 150-156.
2. С.В. Зеленов, С.А. Зеленова, А.С. Косачев, А.К. Петренко. Генерация тестов для компиляторов и других текстовых процессоров II Программирование, Москва. -2003.-29. -№2. -с. 59-69.
3. С.В. Зеленов, С.А. Зеленова, А.С. Косачев, А.К. Петренко. Применение модельного подхода для автоматического тестирования оптимизирующих компиляторов II http://www.citforum.ru/SE/testing/compilers/
4. А.К. Петренко и др. Тестирование компиляторов на основе формальной модели языка // Препринт института прикладной математики им. М.В Келдыша. 1992. №45.
5. A .Aho, R. Sethi, J. D. Ullman. Compilers. Principles, Techniques, and Tools II Addison-Wesley Publishing Company, Inc. - 1985.
6. R. A. DeMillo, A. J. Offut. Constraint-Based Automatic Test Data Generation //IEEE Transactions on Software Engineering. - 1991. - 17. -№ 9. - p. 900-910.
7. R. F. Guilmette. TGGS: A flexible system for generating efficient test case generators.
1999.
8. J. Harm, R. Lammel. Two-dimensional Approximation Coverage // Informatica Journal. -
2000.-24.-№3.
9. R. Lammel. Grammar testing II InProc. of Fundamental Approaches Software Engineering. -2001. -2029. - p. 201-216.
10. R. Lammel, C. Verhoef. Cracking the 500-Language Problem // IEEE Software. - 2001. -18. -№6. -p. 78-88.
11. P.M. Maurer. Generating test data with enhanced context-free grammars II IEEE Software. - 1990. - p. 50-55.
12. P.M. Maurer. The design and implementation of a grammar-based data generator II Software Practice and Experience. - 1992. - 22(3). - p. 223-244.
13. W. McKeeman. Differential testing for software. //Digital Technical Journal. - 1998. -10(1).-p. 100-107.
14. M. Memik, G. Gerlic, V. Zumer, B. R. Bryant. Can a parser be generated from examples? II Symposium on Applied Computing, Proceedings of the 2003 ACM symposium on Applied computing, Session: Programming languages and object technologies, ISBN: 1-58113-624-2 -2003. - p. 1063-1067.
15. A. J. Offut, S. D. Lee. An Empirical Evaluation of Weak Mutation // IEEE Transactions on Software Engineering. - 1994. - 20. -№ 5. - p. 337-344.
16. A. J. Offut, R. H. Untch. Mutation 2000: Uniting the Orthogonal II Mutation 2000: Mutation Testing in the Twentieth and the Twenty First Centuries, San Jose, CA. -October, 2000. - p. 45-55.
17. A. K. Petrenko. Specification Based Testing: Towards Practice IILNCS. -2001. - 2244.
- p. 287-300.
18. P. Purdom. A Sentence Generator For Testing Parsers // BIT. 1972. -№ 2. - p. 336-375.
19. GCC. http://gcc.gnu.org/
20. JavaCC. https://javacc.dev.java.net/
21. J@T. http://unitesk.com/products/jat/
22. CTesK. http://unitesk.com/products/ctesk/
23. ’’The mpC Programming Language Specification”, The Institute for System Programming of Russian Academy of Science, http://www.ispras.ru/~mpc.