Предисловие Специальный выпуск трудов Института системного программирования, который Вы держите в руках, состоит из двух частей и содержит работы сотрудников отдела технологий программирования, посвященные различным методам тестирования сложных систем. В этом выпуске мы постарались представить информацию практически обо всех работах, которые ведутся на данный момент в отделе. К сожалению, это не получилось сделать в полной мере, и ряд разработок остались за рамками сборника, прежде всего, работы по тестированию телекоммуникационных протоколов и систем интеграции уровня предприятия. Однако набор тем статей все же позволяет получить некоторое представление о текущей деятельности отдела. Разнообразие затронутых в сборнике предметов связано с множеством задач, с которыми приходится сталкиваться при оценке качества сложной программной или аппаратной системы. Тем не менее, и из чисто эстетических, и из прагматических соображений, хотелось бы иметь дело с некоторым единым, унифицированным подходом к тестированию. Несколько лет назад, еще не столкнувшись с большинством проблем, играющих важную роль в текущих проектах, мы полагали, что такой подход у нас есть — это технология UniTESK, предназначенная для автоматизации разработки тестов на основе формальных спецификаций. Какие-то элементы этого взгляда вполне подтвердились, например, удалось использовать технологию UniTESK для тестирования модулей микропроцессоров. Но сейчас мы знаем больше как о различных методах тестирования, так и о разнообразных внешних ограничениях, которые могут в значительной степени определять используемые в проектах технологии, и недостаточная универсальность положений, лежавших в основе нашего подхода, становится все более очевидной. Тем не менее, некоторую эклектичность тех методик и технологий, которые разрабатываются и используются сейчас, мы склонны рассматривать не как финальное состояние, а как переход к новому этапу унификации, позволяющему более целостно осмыслить проблемы обеспечения качества для сложных систем. Итогом должно стать создание «новой версии» подхода UniTESK, унифицирующего методы тестирования в значительно более широкой области и на новом уровне. Пока контуры такого подхода лишь едва проглядывают за многообразием используемых техник и методов. Анализ ситуации проводит А. К. Петренко в первой статье первой части сборника «Унификация в автоматизации тестирования. Позиция UniTESK». Вторая и третья статьи сборника посвящены теории, лежащей в основе методов тестирования компонентов распределенных систем. И. Б. Бурдонов и А. С. Косачев в первой статье «Системы с приоритетами: конформность, 5
тестирование, композиция» формализуют понятие взаимодействия с приоритетной обработкой сообщений различных типов и строят теорию тестирования для компонентов, взаимодействующих подобных образом. Во второй своей статье «Эквивалентные семантики взаимодействия» они анализируют наборы тестовых возможностей, предоставляемых различными семантиками взаимодействия, и определяют условия эквивалентности таких семантик, т.е. условия, при выполнении которых их возможности с точки зрения тестирования одни и те же. Тему тестирования компонентов распределенных систем продолжает статья В. С. Мутилина «Многопоточное тестирование программных интерфейсов». В ней представлен метод проверки программ на безопасность их работы в многопоточном окружении, сочетающий элементы тестирования и дедуктивного анализа программного кода. Две следующих статьи рассказывают о критериях тестового покрытия, используемых при тестировании на основе формальных спецификаций, и техниках вычисления достигнутого покрытия во время тестирования. Статья В. В. Кулямина «Критерии тестового покрытия, основанные на структуре контрактных спецификаций» определяет несколько удобных для использования критериев покрытия на базе спецификаций. Статья С. В. Зеленова и С. А. Зеленовой «Автоматическое определение выполнимости наборов формул для операций сравнения» посвящена алгоритму, позволяющему отбрасывать невыполнимые комбинации значений формул, использующих сравнения чисел, получая таким образом более точные отчеты о достигнутом тестовом покрытии. Еще одна статья С. В. Зеленова и С. А. Зеленовой «Генератор сложных данных Pinery: реализация новых возможностей UniTESK», написанная в соавторстве с А. В. Демаковым, представляет инструмент Pinery для генерации сложных тестовых данных и различные техники его использования, позво-ляющие точнее нацелить процесс генерации на создание специфических данных. Статья С. Г. Грошева «Локализация ошибок методом сокращенного воспроизведения трассы» описывает методику минимизации сложных тестов для облегчения анализа и более аккуратной локализации обнаруживаемых ими ошибок, а также инструмент, позволяющий строить минимальные тесты автоматически. В последней в первой части статье Е. С. Чернова и В. В. Кулямина «Тестирование современных библиотек тригонометрических функций» рассказывается о результатах тестирования нескольких реализаций тригонометрических функций с помощью разработанных в ИСП РАН общих подходов к проверке правильности работы математических библиотек. Член-корреспондент РАН В. П. Иванников Кандидат физ.-мат. наук В. В. Кулямин 6
Унификация в автоматизации тестирования. Позиция UniTESK А. К. Петренко
[email protected] Аннотация. Рассматриваются современные тенденции унификации в средствах автоматизации тестирования, анализируется текущий уровень унификации в инструментах семейства UniTESK и очерчиваются возможные формы унификации, которые позволят расширить круг возможностей технологии.
1. Введение. Специализация и унификация в развитии UniTESK Первые работы по созданию новой технологии тестирования, которая вскоре была названа UniTESK, начались в ИСП РАН во второй половине 1999 года. Само название включало в себя один из корней слова унификация (UNIfication), а в целом расшифровывалось как Unified TEsting Specification toolKit. Унификации в UniTESK постоянно придавалось большое значение. Одним из важных элементов унификации был принцип использования однородного спецификационного расширения языков программирования — во всех расширениях использовалась одна и та парадигма спецификации (контрактные спецификации) и единообразная архитектура теста, включающая одинаковые механизмы взаимодействия тестовой системы с целевой системой. Этот принцип позволил, следуя одной общей методике, создавать тесты для программного обеспечения на различных языках программирования. В основу новой технологии был положен опыт разработки и использования технологии KVEST [1] и опыт ее использования в задачах тестирования программного интерфейса (API, Application Program Interface) ядра операционной системы компании Nortel Networks [2]. Кроме того, рассматривались сценарии применения новой технологии при тестировании реализаций телекоммуникационных протоколов и других распределенных и компонентных систем. Предшественник UniTESK, технология KVEST, была в некотором роде «еще более унифицированной». Она в качестве языка формального описания функциональных требований и тестовых сценариев использовала язык формальных спецификаций RSL [3], что, в принципе, 7
позволяло разрабатывать спецификации и тесты независимо от языков программирования, которые используются в реализации. Опыт KVEST показал, что в чем-то принятые принципы унификации оказались достаточно удачными, а в чем-то совсем неприемлемыми. К удачным можно отнести подход к описанию функциональных требований в форме контрактных спецификаций — в виде ограничений типа пред- и постусловия и ограничений на типы данных. К неудачным приходится отнести принцип использования единого, универсального языка спецификаций. Причем первым основанием для критики такой декларации является совсем не ограниченность возможностей языка (хотя RSL достаточно консервативный язык, он не имеет объектно-ориентированных возможностей, в нем практически нет средств для описания темпоральных свойств и др.). Главным тезисом критики RSL является его непохожесть на распространенные языки программирования, что сразу вызывает сложности как в обучении будущих пользователей, так и в совместном использовании программ на языке RSL и языке программирования во время разработки и отладки теста. Выбирая принципы унификации UniTESK, мы сделали небольшой шаг назад, сказав, что языки спецификаций для разных языковых платформ реализации могут быть разными. Так появилось семейство однородных спецификационных расширений для разных языков: для Java — J@va, для языка C — SEC (Specification Extension of C) и Chase — спецификационное расширение для C#. Одновременно с разработкой новых инструментов семейства UniTESK мы расширяли круг использования этих инструментов. Поскольку о тестировании реализаций протоколов мы думали заранее, опыт применения CTESK (UniTESK для C-платформы) не потребовал кардинальных переделок в инструменте. Однако при анализе возможностей UniTESK-инструментов для системного тестирования компиляторов стало ясно, что для этого требуются новые техники, так появились инструменты BNF tool и ОТК. Затем были попытки применения UniTESK для тестирования графических пользовательских интерфейсов и их разновидности — Web-интерфейсов. Часть попыток была вполне успешной, тесты удавалось построить достаточно быстро, их было легко модернизировать и синхронизовать с эволюционирующей реализацией. Однако в общем случае, оказалось, что помимо чисто технических проблем управления графическим интерфейсом имеются и принципиальные ограничения UniTESK в плане возможностей по описанию/спецификации пользовательского интерфейса как такого. В последние 3 года спектр работ, в которых проводились эксперименты по применению UniTESK, резко расширился. В него вошли тестирование распределенных платформ интеграции крупных информационных систем, тестирование ОС реального времени, тестирование библиотек ОС Linux, тестирование системы поддержки времени исполнения Java, реализаций 8
протоколов Mobile IPv6, IPsec, IPMP и др. Расширение спектра приложений, естественно, повлекло необходимость развития имевшихся инструментов UniTESK, необходимость разработки новых и использования инструментов, имеющихся на рынке. В итоге иногда складывается впечатление, что для каждой новой задачи приходится искать (а, если найти не удается найти, то создавать) новый инструмент. С одной стороны, специализация — это тоже известный путь развития. Не бывает универсальных самолетов, автомобилей, даже в более прозаических вещах, таких как ложки и ножи, также нет универсальных решений. Однако универсальность и унификация — это не одно и тоже. Унификация — это средство, позволяющее на основе некоторого типового набора возможностей получить решение широкого круга задач. Интуиция подсказывает, что в тестировании может быть построен набор средств, достаточно ограниченный, из которых как из Lego-конструктора можно собрать нужный инструмент. Анализу достигнутого уровня унификации в автоматизации тестирования и направлениям развития унификации и посвящается данная статья.
2. Унификация в программировании и в автоматизации тестирования Начнем с определения термина «Унификация». Унификация (от лат. unus — один и facio — делаю) — приведение к единообразию, к единой форме или системе (Большой энциклопедический словарь, М., БСЭ, 1964). Унификация — в технике, приведение различных видов продукции и средств её производства к рациональному минимуму типоразмеров, марок, форм, свойств и т.п. Основная цель У. — устранение неоправданного многообразия изделий одинакового назначения и разнотипности их составных частей и деталей, приведение к возможному единообразию способов их изготовления, сборки, испытаний и т.п. У.— важное направление в развитии современной техники, комплексный процесс, охватывающий вопросы проектирования, технологии, контроля и эксплуатации машин, механизмов, аппаратов, приборов. В условиях научно-технической революции принципы У. используют не только в отраслях производства, но и в др. сферах человеческой деятельности... (Большая советская энциклопедия. БСЭ, 1969-1978 [4]). В качестве ключевых понятий, которые определяют цели и формы унификации, авторы этой статьи называют: взаимозаменяемость; типизация; построение агрегатных унифицированных систем;
9
качество выпускаемой продукции, её надёжность и долговечность благодаря более высокой технологичности конструкции изделий и проработанной технологии их изготовления; стандартизация; единые системы технической документации; специализация производства. Похоже, авторы БСЭ затронули практически все моменты, которые являются ключевыми в автоматизации тестирования. Пожалуй, по понятным причинам, осталась не затронутой только одна тема — интероперабельность компонентов тестовых систем.
2.1. Унификация в использовании и в разработке средств тестирования Каков опыт унификации в программировании, каковы удачные примеры унификации в программировании? Самый первый общепризнанный результат унификации — это библиотеки программ (раньше они назывались библиотеками стандартных программ). Собственно, с создания библиотек и средств их использования началось программирование как новая научная и техническая дисциплина. Следующим направлением унификации можно назвать стандартизацию языков программирования. Второй, возможно, не такой заметный, но чрезвычайно важный пример унификации — это определение стандартных представлений программ, позволяющих объединять объектные модули, оттранслированные с различных языков программирования в единую программную систему. Это как раз тот случай, когда мы не можем предложить совершенно общего, универсального решения при выборе языка программирования, но имеем средства унификации, чтобы, комбинируя модули на разных языках, решить любую стоящую перед нами задачу. Хороший пример унификации — общая схема построения компиляторов, которая предусматривает фазы лексического, синтаксического, семантического анализа, вместе они образуют так называемый front-end компилятора, фазы оптимизации и генерации кода, которые вместе образуют так называемый back-end. Такая разбивка на фазы позволяет не только упростить изучение устройства компиляторов, становится реальной возможность построения компилятора методом сборки отдельных компонентов. Можно собрать несколько фронт-ендов (для разных языков) и несколько бэк-ендов для разных аппаратных платформ и получить компилятор, который объединяет в себе наилучшие качества, реализованные в его отдельных модулях. Унификация в компиляторах проводится и на уровне инструментов разработки компиляторов. Например, имеются средства типа Lex и Yacc, при помощи которых можно сгенерировать модули фронт-енда. Рассматривая вопросы унификации в автоматизации тестирования, следует обратить внимание на то, что имеется два ракурса этого рассмотрения. Первый ракурс — это позиция разработчика тестов, второй — позиция 10
разработчика средств автоматизации тестирования. Начнем с анализа позиции тестировщика, или разработчика тестов. Переходя от проекта к проекту или переключаясь с разработки тестов для одной системы к другой, тестировщик отвечает на однотипные вопросы: На каких входных данных нужно проверять систему? Что является критерием правильности функционирования системы? Что является критерием полноты тестирования? и др. В зависимости от особенностей интерфейса системы, от возможностей взаимодействия с системой (например, за счет прямого доступа к ее внутреннему состоянию), от требований заказчика и других условий тестировщик выбирает (или получает в свое распоряжение) набор инструментальных средств для разработки тестов. По крайней мере, из соображений экономии времени, очевидно, что разработка тестов будет выполняться быстрее, если в новом инструментарии тестировщик увидит привычные средства с привычными возможностями. Это возможно только при условии, что при разработке различных инструментов следуют некоторой общей схеме унификации1. Теперь вернемся к унификации с точки зрения разработки самих инструментов автоматизации тестирования. По сути, все инструменты автоматизации тестирования выполняют ограниченный набор операций. Сложность этого набора можно сравнить со сложностью компиляторов, недаром, некоторые генераторы тестов называются трансляторами (например, изначальное название генератора тестов ADLT расшифровывалось как Assertion Definition Language Translator [5]). Если аналогия верна, то, казалось бы, что в распоряжении разработчиков инструментов генерации тестов уже давно должны были появиться генераторы входных тестовых данных, генераторы тестовых оракулов, выносящих вердикт на основе того или иного представления требований, анализаторы тестового покрытия в соответствии с тем или иным критерием покрытия и так далее. У разработчиков компиляторов есть еще один «инструмент унификации» — это компилятор GCC, который можно рассматривать как коллекцию типовых решений задач разработки компиляторов. На основе такой «коллекции» многие строят свои собственные компиляторы методом инкрементальных модификаций. Какова же ситуация с унификацией в нашем случае? Ответ пугает своей категоричностью — инструментов, которые позволяли бы воспользоваться типовыми решениями перечисленных выше задач тестирования, нет. Одной из причин этого служит то, что нет единого подхода ни к форме представления
1
Заметим, что временной фактор в оценке эффективности процесса разработки тестов не является единственным и, возможно, не является главным. Унификация ведет к постоянному росту профессионального мастерства, что, в свою очередь, ведет к повышению качества тестирования. 11
входных данных для программ таких библиотек, ни к форме представления их результатов.
2.2. Обзор работ по унификации и стандартизации средств тестирования Тема унификации технологий и инструментов тестирования широко и активно обсуждается. Сейчас трудно сказать, является ли этот интерес некоторой модной темой или в программной индустрии имеется реальная заинтересованность, подпитывающая исследования и разработки (одним из оснований для сомнений в реальной заинтересованности является то, что активность в этой области заметна в Европе и мало заметна в США и Азии). Большая часть исследований и публикаций по этой теме так или иначе опираются на UML 2.0 Testing Profile (U2TP) [6], который развивается Консорциумом U2TP [7]. Это профиль унифицированного языка моделирования систем UML 2.0. U2TP претендует стать стандартом в данной области и является развитием подхода Model Driven Architecture (MDA), который продвигается группой OMG [8]. В Консорциум U2TP входят такие известные компании как Ericsson, IBM, Motorola, Telelogic и один из ведущих немецких исследовательских институтов Fraunhofer FOKUS. U2TP ограничивает спектр рассмотрения процессов тестирования тестированием по методу «черного ящика». Сам он рассматривается как систематическая модель для описания компонентов тестовых систем и их возможных взаимодействий в ходе выполнения тестов с помощью конструкций UML 2.0 и базируется на метамодели UML. Такие описания в дальнейшем используются как артефакты, позволяющие автоматизировать часть работ по созданию и исполнению тестов. Одна из задач U2TP состоит в систематизации терминов, которые используются при описании архитектуры тестов, целей тестирования (test purposes), тестовых вариантов (test cases) и средств их привязки к тестируемым системам (адаптеров), процессов тестирования и участников процесса. Помимо инициативы U2TP в Европе имеется еще несколько проектов, например, два проекта из программы ITEA (Information Technology for European Advancement) [9], которая, в свою очередь является ветвью программы Eureka [10]. EAST/EEA (Electronics Architecture and Software Technology — Embedded Electronic Architecture) [11]. Суть этого проекта, объединяющего 23 партнера из 4 стран, — разработка профиля UML 2.0, который станет языком описания архитектуры программноаппаратных систем в автомобильной промышленности. В частности, отдельным разделом в разрабатываемом профиле выделены процессы верификации и валидации, включающие тестирование, и набор модельных компонентов, используемых в рамках этих процессов. 12
Проект TT-Medal [12], цель которого — продвижение наукоемких методов тестирования, в особенности с использованием языка TTCN3, в промышленную практику европейских компаний. Практическим результатом проекта стала интеграция трех инструментов: TTworkbench [13], включающего компилятор TTCN-3, среду разработки тестов в Eclipse, генераторы кодеков; Classification Tree Editor (CTE) [14], средство для ручного построения классов эквивалентности в системах с большим числом параметров и генератор тестов финской компании Conformiq [15]. В рамках данного проекта рассматривались различные способы построения интерфейса между тестирующей системой и тестируемой. В проекте была продемонстрирована возможность использования описаний интерфейсов на ASN.1, IDL, и XML. Стоит упомянуть еще два проекта из программы ITS Framework Programme 6. HIDENETS (Highly Dependable IP-based Networks and Services) [16] нацелен на разработку устойчивой к отказам архитектуры (resilience solutions) для распределенных и мобильных систем (mobility-aware services). Так же, как и в других проектах, которые мы здесь рассматриваем, большое внимание уделяется не только самим целевым системам, но инфраструктуре для моделирования, верификации и валидации. Этим объясняется использование в проекте U2TP. В контексте данной статьи интересным является полный обзор стандартов, которые затрагивают вопросы унификации и переиспользования артефактов как ПО в целом, так и артефактов из области тестирования. В частности, достаточно подробно анализируется RAS: Reusable Asset Specification [17], также разработанный под патронажем OMG. ATESST (Advancing Traffic Efficiency and Safety through Software Technology) [18]. Одним из результатов этого проекта является отчет по теме Linking Requirements and Test Artifacts [19]. В нем подробно перечисляются различные способы выделения требований и их привязки к различным артефактам и процессам проектов, включая тесты и их отдельные компоненты. В качестве объективной оценки достижений в данной области можно сослаться на то, что многие работы, которые изначально поддерживались европейскими программами, теперь нашли свое продолжение в рамках альянса AUTOSAR [20], который финансируется индустриальными партнерами. Сейчас список партнеров альянса включает практически всех крупных поставщиков автомобилей и электроники для них. Вместе с тем, имеется по крайней мере два аспекта, в которых результаты активности, связанной с U2TP, можно было оценить как недостаточно значимые (естественно, здесь мы рассматриваем только проблемы тестирования): спектр рассматриваемых проблем и подходов к их решению, а 13
также доведение результатов до реальных техник, технологий, программных продуктов и автоматизированных сервисов. Начнем с последнего аспекта. Сейчас основной формой практических результатов являются стандарты, описывающие архитектуры и процессы тестирования. Пока эти стандарты являются некоторыми рекомендациями, они не обрели четкой и строгой формы, необходимой для того, чтобы быть переведенной на язык интерфейсов компьютерных систем. Причем, наряду с организационными трудностями, которых всегда много при создании новых стандартов, похоже, есть трудности, обусловленные нестыковкой подходов при описании артефактов и ролей на абстрактном уровне и реальными технологиями, методами и инструментами, которые используются в производстве. Возможно, корни этой проблемы лежат в сфере первого из указанных выше аспектов, а именно в том, что идеологически подход U2TP и язык разработки тестов TTCN-3 следуют логике разработки ручных тестов и практически не уделяют внимания собственно методам генерации тестов. По различным соображениям приверженцы U2TP пытаются сохранить представление о тестовом варианте (test case) как о простом сценарии. Сложные модели, из которых могут извлекаться тесты, в концепции U2TP просто не рассматриваются. Профиль ограничивается описанием компонентов самих тестов, без всякой оглядки на способы их получения. Идея «черного ящика» как единственного рассматриваемого подхода к тестированию также позволяет разработчикам U2TP закрывать глаза на проблемы анализа структуры реализации, без чего нельзя решить, например, такие важные задачи как построение модели тестового покрытия и модели ошибок, учитывающей особенности реализации и хорошо зарекомендовавшие себя на практике эвристики. Эти ограничения в постановке задачи унификации не позволяют решить ее с использованием наиболее перспективных подходов к автоматической генерации тестов. В частности, на основе U2TP невозможно обеспечить интеграцию различных передовых техник верификации и валидации программ, таких как проверка моделей (model checking), верификационный мониторинг2 (run-time verification), генерация тестов методами статического анализа или с помощью генетических алгоритмов и др. Сложность интеграции этих методов обусловливается тем, что при эффективном решении задачи автоматической генерации тестов для реальных больших систем часто нельзя выделять решение отдельных задач генерации в модули, достаточно слабо связанные друг с другом. Простейший пример: пусть нам надо отобрать представителей различных классов эквивалентности из некоторого
2
Русский перевод В. В. Куляминым. 14
термина
«run-time
verification»
предложен
дискретного многомерного пространства, являющегося областью определения некоторой функции. Логически задачу можно разбить на следующие три шага. Построить все наборы значений — точки пространства. Разбить полученное множество на классы эквивалентности. Выбрать из каждого класса по одному произвольному представителю.
На практике такой последовательный подход и, соответственно, простая схема композиции модулей, которые отвечают за выполнение каждого шага, не работают по простой причине — число точек в области определения, хотя и конечно, но не может уместиться ни в каких разумных вычислительных ресурсах. Вместе с тем эта задача имеет решение, которое требует как можно более прямой генерации наборов значений в заданных классах эквивалентности. В заключение данного критического анализа хотелось бы отметить, что хотя концептуально важно отделять друг от друга методы тестирования на основе «белого ящика» и на основе «черного ящика», и хотя тестирование на основе моделей, конечно, является одним из методов «черного ящика», но технически автоматическая генерация тестов на основе моделей во многом не отличается от методов генерации на основе исходных кодов (или другого представления реализации). Поэтому декларация U2TP об использовании в качестве базового подхода исключительно метода «черного ящика», будучи понятой буквально, является барьером на пути более полной автоматизации генерации тестов.
работы этого теста. Более детальное рассмотрение этой задачи вскрывает новые вопросы: что является источником критериев правильности, удается ли, и как именно, описать такие критерии в общем виде, как они трансформируются в оракул, как выносится общий вердикт теста, если в нем несколько оракулов, и т.п. Определение критериев полноты тестирования. Сбор информации о достигнутом покрытии. Использование информации о достигнутом покрытии для управления ходом генерации. Собственно генерация тестовых воздействий, которая распадается на следующие подзадачи. o Построение последовательности тестовых воздействий в целом. o
Выбор вызываемой операции в рамках одного воздействия, если интерфейс предоставляет несколько операций.
o
Генерация входных данных операции.
3. Специализация и унификация в UniTESK. Текущее состояние
3.1. Задачи построения тестов В работах по методам тестирования и по автоматизации тестирования затрагиваются многочисленные задачи, которые необходимо уметь решать при создании тестов. Примерный список этих задач представлен ниже. Определение требований к системе, выделение из них условий корректного воздействия на целевую систему и критериев правильности результатов3. Построение оракула — компонента теста, выносящего вердикт о выполнении или нарушении требований на основе результатов
3
Некоторые теоретики программной инженерии рассмотрение этого вопроса отнесли бы к дисциплине, которая называется управлением требованиями (requirements management), однако на практике, даже если в начале проекта требования сформулированы, тестировщики должны к ним отнестись критически и проанализировать, насколько имеющиеся формулировки требований пригодны как основа для разработки тестов. Часто проектирование тестов влечет переформулировку и пополнение требований. 15
16
Простейшая итерация данных из области определения операции, без анализа других моделей (требований, ошибок, структуры системы). Итерация простых структур данных. Итерация иерархических структур данных. Нацеливание итерации входных данных при использовании моделей требований, ошибок или структуры тестируемой системы. Статическое вычисление входных данных для достижения покрытия ситуаций в различных моделях, в том числе и в структуре системы.
o
Отбрасывание некорректных тестовых данных.
o
«Прореживание» тестовых данных, то есть обеспечение покрытия того же множества существенно различных в соответствии с используемыми критериями покрытия ситуаций при помощи меньшего количества отдельных воздействий.
Генерация отчетов о тестовом покрытии. o Отчеты на основе каталогов неформальных требований. o
Отчеты на основе структурных характеристик реализации (для различных метрик тестового покрытия).
o
Отчеты на основе формальных моделей, при этом рассматриваются модели реализации, модели требований, модели ошибок. Примерами возможных видов моделей
являются контрактные спецификации, исполнимые модели (конечные автоматы, LTS, сети Петри, MSC диаграммы и пр.), грамматики и производные от них структуры, например графы зависимости и деревья вывода, другие формализмы описания структурированных данных (SQL, XML scheme, Relax NG, ASN 1.0 и др.).
Связь теста с целевыми интерфейсами, в том числе конвертация данных, их транспорт, marshalling, перехват событий; эти задачи часто связываются с элементами тестовых систем, которые называют адаптерами, медиаторами, брокерами, proxy и др. Служебные задачи, например, сбор трассировочной информации для целей анализа обнаруженных ошибок или вычисления достигнутого покрытия, то есть для генерации отчетов различных видов; конфигурационное управление системой тестов и ассоциированной с ними информации (каталоги требований, об ошибках, запросы на их исправление, трассировка процесса устранения ошибок и т.п.). Попробуем рассмотреть материал данного сборника сквозь призму унификации систем автоматизации разработки тестов. Заметим, что статьи сборника представляют достаточно широкий спектр походов и техник. Так в работах Д. Силакова и В. Мутилина за основу взят метод «белого ящика», используется инструментация исходного кода, все остальные работы — классический пример использования подхода «черного ящика». В работах А. Демакова, С. Зеленова, С. Зеленовой, Р. Зыбина, В. Кулямина, А. Пономаренко, В. Рубанова и Е. Чернова тесты строятся, в основном, на основе изучения области входных данных. В. Рубанов, А. Хорошилов, Е. Шатохин представляют технологию традиционной ручной разработки тестов, в статьях А. Камкина и М. Чупилко описывается подход к тестированию модулей микропроцессоров. Часть работ использует прием тестирования на лету (testing-on-the-fly, генерацию тестовых данных и воздействий в ходе выполнения теста). Повышению эффективности локализации ошибок по результатам тестирования на лету посвящена статья С. Грошева. В статье А. Камкина рассматриваются методы статической генерации тестов (тестовых программ), в статье В. Кулямина — методы определения критериев тестового покрытия, в статьях И. Бурдонова и А. Косачева разрабатывается формальный подход к определению понятия конформности — соответствия поведения системы некоторой модели — и методы проверки этого соответствия. Возьмем, например, первые два пункта из списка задач, приведенного выше. Определение требований к системе. Построение оракула. В работах, описывающих разработку тестов на соответствие стандарту LSB, подробно рассматриваются выделение требований для тестов проверки базовой работоспособности (инструмент Azov) и для тестов традиционного 17
типа (инструмент T2C). Кроме того, в этих же работах упоминаются тесты более высокого качества для LSB Core, созданные в рамках проекта OLVER [21] при помощи инструмента CTESK. Все эти работы используют общую базу данных, в которой хранятся разнообразные данные об интерфейсах. Данные о сигнатурах интерфейсных функций используются всеми тремя инструментами — здесь унификация уже достаточно высокого уровня. Вместе с тем, в работе по модульному тестированию микропроцессоров, где используется тот же инструмент CTESK, возможности единообразного хранения требований к интерфейсам не использованы. Рассматривая те же работы в плане унификации механизма вынесения вердикта можно обнаружить, что три инструмента, использующиеся для разработки тестов для LSB строят оракул совершенно по-разному, то есть в этом аспекте унификации пока нет.
3.2. Вектор развития UniTESK Если рассмотреть все задачи, которые необходимо решить при автоматизации создания тестов, то для каждой из них в отдельности можно предложить некоторую систематизацию подходов к решению этих задач. Разработка такой системы — это первый шаг к унификации. Но, похоже, что этого шага пока не сделали разработчики U2TP — они предложили номенклатуру компонентов, из которых состоят тестовые системы, но не сделали попытки более подробного описания каждой сущности, попавшей в номенклатуру. После первого шага должен следовать второй. Он состоит в разработке шаблонов проектирования. Это нелегкая задача, но здесь она еще более усложняется из-за того, что эти шаблоны должны учесть варианты совместного и согласованного решения нескольких задач автоматизации тестирования. Примером таких взаимосвязанных задач является целенаправленная генерация тестовых данных для покрытия заданных операторов или заданного пути в коде и «прореживание» комбинаций входных данных, если их общее количество слишком велико. При этом стратегия генерации и стратегия «прореживания» часто должны динамически адаптироваться в ходе тестирования. Шаблоны согласованных решений должны появиться и для тех случаев, когда можно совместно использовать разные техники тестирования или верификации, например, элементы проверки моделей (model checking) и статического анализа с применением инструментов, автоматически находящих данные, удовлетворяющие сложным ограничениям (constraint solvers). Другое место, где потребуются специальные усилия для согласования разных подходов — использование нескольких разных моделей, служащих для описания требований, поведения, гипотез о возможных ошибках и т.п. На первый взгляд может показаться, что задача унификации, если ее решать достаточно основательно, становится неоправданно сложной. Но судить об этом нужно, соотнося трудоемкость ее решения и эффект, который может 18
быть получен. Результат, к которому должна привести унификация средств разработки инструментов автоматизации тестирования, с одной стороны, должен обеспечить совместимость компонентов тестов друг с другом, а, с другой стороны, выведет нас на возможность интеграции различных методов и инструментов верификации. Сколько шагов унификации уже пройдено технологией UniTESK? Прямого ответа на этот вопрос пока дать нельзя, но что-то уже сделано. Есть задел в плане введения единой номенклатуры артефактов, ролей и процессов тестирования. Наработан некоторый опыт унифицированных решений для разработки тестов совершенно разного плана (например, база данных интерфейсов для систем, тестирующихся при помощи инструментов Azov, T2C, CTESK). Начаты работы по интеграции JavaTESK и генератора сложных структур данных Pinery, ведется работа по интеграции Pinery и алгоритмов «прореживания» на основе покрывающих наборов [22]. То есть, сейчас есть некоторый опыт в унификации как в рамках программы первого шага, так и второго, однако полной картины унифицированной схемы разработки инструментов тестирования пока нет. Создание такой схемы — это и есть новый вектор развития технологии.
4. Заключение В статье рассматривались проблемы и перспективы унификации в области разработки систем автоматического построения тестов. Были перечислены положительные и отрицательные моменты, сопровождающие любую унификацию и унификацию разработке программных систем, в частности. Были выделены два ракурса унификации в автоматизации тестирования: позиция разработчика тестов и позиция разработчика инструментов создания тестов. Далее в статье был проанализирован опыт в унификации систем тестирования сконцентрированной вокруг работ по использованию MDA подхода, реализованного в U2TP — UML 2.0 Testing Profile. Была показана ограниченность постановки задачи, определенной авторами U2TP. На примере работ, представленных в сборнике, показано, какие решения в рамках разрабатываемых в ИСП РАН технологий уже в некоторой степени унифицированы, какие пока далеки от унификации. Отмечено, что унификация в автоматизации тестирования может проводиться на основе компонентного подхода. Это подразумевает, что должны выделяться модули, играющие те или иные роли, должны описываться интерфейсы этих модулей, типовые шаблоны проектирования (patterns), которые на основе стандартизованного набора модулей позволят строить инструменты, с учетом как специфики тестируемой системы, так и методов спецификации и тестирования, выбранных в данном инструменте. Заключая статью, хотелось бы, во-первых, отметить, что задача унификации в автоматизации тестирования расценивается как в высшей степени актуальная, во-вторых, что опыт европейских исследователей и специалистов показывает 19
на трудности, которые встают при попытках проведения унификации методом «сверху-вниз». Вероятно, коренная причина здесь в идеологии подхода MDA — реализацию можно построить на основе модели, если адекватная модель, или хотя бы схема моделирования уже есть. Такое бывает лишь в областях, где накоплен большой опыт разработки типовых решений. Поскольку область автоматической генерации тестов бурно развивается и проблем в ней еще больше, чем решений, здесь пока таких моделей нет, и не понятно, когда они появятся. Констатация этого факта не означает, что унификация невозможна. Она возможна, но подходить к решению этого вопроса надо с двух сторон: от общего к частному, как в MDA, и от частного к общему, как это всегда происходит в программировании, если разработчик постоянно думает об обобщении своего опыта, о выделении тех модулей и тех операций, которые повторяются многократно в разных проектах, при решении разных задач. Замечу, что объектами такого переиспользования могут быть не только программные модели (что каждый раз является настоящей удачей), такими объектами могут быть шаблоны проектирования, протоколы взаимодействия отдельных компонентов и другие артефакты и процессы, которые составляют как сами системы автоматизации тестирования, так и процессы их разработки и эксплуатации.
5. Благодарности Автор выражает признательность А. В. Бойченко за предоставление большого набора материалов по стандартизации профилей процессов тестирования и В. В. Кулямину за многочисленные обсуждения проблем унификации в тестировании. Литература [1] I. Burdonov, A. Kossatchev, A. Petrenko, D. Galter. KVEST: Automated Generation of Test Suites from Formal Specifications. Proceedings of Formal Method Congress, Toulouse, France, 1999, LNCS 1708:608–621, Springer-Verlag, 1999. [2] http://www.nortel.com. [3] The RAISE Language Group. The RAISE Specification Language. Prentice Hall Europe, 1992. [4] http://slovari.yandex.ru/dict/bse. [5] http://adl.opengroup.org/exgr/icse/icse98.htm. [6] http://www.omg.org/technology/documents/formal/test_profile.htm. [7] http://www.fokus.fraunhofer.de/u2tp/index.html. [8] http://www.omg.org. [9] http://www.itea2.org. [10] http://www.eureka.be. [11] http://www.east-eea.net. [12] http://www.tt-medal.org/. [13] http://www.testingtech.de/products. [14] http://www.systematic-testing.com/functional_testing/cte_main.php?cte=1. [15] http://www.conformiq.com. [16] http://www.hidenets.aau.dk/.
20
[17] http://www.omg.org/technology/documents/formal/ras.htm. [18] http://www.atesst.org. [19] http://www.atesst.org/scripts/home/publigen/content/templates/show.asp?P=114&L=EN &ITEMID=5. [20] http://www.autosar.org. [21] http://linuxtesting.org/. [22] C. J. Colbourn. Combinatorial aspects of covering arrays. Le Matematiche (Catania) 58:121–167, 2004.
21
Системы с приоритетами: конформность, тестирование, композиция И. Б. Бурдонов, А. С. Косачев {igor, kos}@ispras.ru Аннотация. В статье представлен подход к моделированию компонентов распределенных систем, взаимодействие которых построено на обработке событий с учетом их приоритетов. Несмотря на широкое использование на практике приоритетной обработки запросов или сообщений, математические модели взаимодействия таких программ чаще всего абстрагируются от приоритетов, вводя излишний недетерминизм в описание их поведения. Предложенный подход пытается избежать этого недостатка за счет определения параллельной композиции, моделирующей взаимодействие такого рода. Основное содержание статьи — построение теории формального тестирования компонентов, использующих приоритеты. В рамках этой теории вводятся понятие безопасного выполнения модели и отношение конформности между моделями, а также рассматриваются вопросы построения наборов тестов, проверяющих конформность.
«Если воняет, то это химия, когда ничего не работает — физика, а если понять нельзя ни слова — математика». Из «Законов Мэрфи», цитируется по публичной лекции В.И. Арнольда «Сложность конечных последовательностей нулей и единиц и геометрия конечных функциональных пространств».
1. Введение В существующих теориях тестирования конформности (conformance testing) подразумевается отсутствие приоритетов между действиями, которые тестируемая система может выполнять в данной ситуации [23]. Это называется правилом недетерминированного выбора действий. В то же время для реальных программных и аппаратных систем это правило не всегда адекватно отражает требуемое поведение системы. Рассмотрим несколько примеров.
23
Выход из дивергенции. Под дивергенцией понимается бесконечная внутренняя активность («зацикливание») системы. Запрос, поступающий извне, может бесконечно долго игнорироваться системой, если он имеет тот же приоритет, что и внутренняя активность. Заметим, что внутренняя активность может быть инициирована предыдущим запросом. Если речь идёт о составной системе, собранной из нескольких компонентов, то такая внутренняя активность может быть естественным результатом взаимодействия компонентов между собой. И в этом случае для обработки запроса, поступающего в систему (в один из её компонентов) извне, он должен иметь больший приоритет, чем внутреннее взаимодействие. Выход из осцилляции (приоритет приёма над выдачей). Под осцилляцией понимается бесконечная цепочка выдачи сообщений системой. Для того, чтобы такую цепочку можно было прервать, заставив систему обрабатывать поступающий извне запрос, последний должен иметь больший приоритет, чем выдача сообщений. Приоритет выдачи над приёмом в неограниченных очередях. Этот обратный пример характерен для неограниченной очереди, используемой в качестве буфера между взаимодействующими системами, в частности, при асинхронном тестировании (тестировании в контексте). Здесь нужно, чтобы выборка из очереди была приоритетней постановки в очередь. В противном случае очередь имеет право только принимать сообщения и никогда их не выдавать. При асинхронном тестировании для входной очереди это означает, что все входные сообщения, посылаемые тестом, не доходят до реализации, бесконечно накапливаясь в очереди. Соответственно, для выходной очереди это означает, что тест может не получать никаких ответных сообщений от реализации, хотя она их выдаёт, поскольку они «оседают» в очереди. Прерывание цепочки действий. Команда «отменить» (cancel) должна останавливать выполнение последовательности действий, инициированной предыдущим запросом, и вызывать цепочку завершающих действий. При отсутствии приоритетов такая команда, даже если она выдана сразу после выдачи запроса, имеет право быть выполнена только после того, как вся обработка закончится, то есть, фактически, ничего не «отменяет». Приоритетная обработка входных воздействий. Если в систему поступает одновременно несколько запросов, то часто требуется их обработка в соответствии с некоторыми приоритетами между ними. Это обычно реализуется в виде очереди запросов с приоритетами или в виде нескольких очередей запросов с приоритетами между очередями. К этому типу приоритетов относится и обработка аппаратных прерываний в операционной системе. Отсутствие приоритетов в моделях систем не даёт возможности проверять при тестировании выполнение тех требований к системе, которые могут быть выражены только в форме приоритетов. В данной статье предлагается способ введения приоритетов в теорию конформности: семантику взаимодействия и модель системы, отношение конформности, методы генерации тестов и 24
оператор композиции (сборки составной системы из взаимодействующих между собой компонентов). Теория конформности без приоритетов кратко описана в нашей статье [13], подробное изложение с доказательствами утверждений содержится в диссертации одного из авторов данной статьи [16], теория конформности для класса так называемых -семантик излагается в книге [15]. Здесь мы сначала повторим основные положения этой теории, а затем модифицируем их для случая приоритетов.
2. Теория конформности без приоритетов 2.1. Семантика взаимодействия и безопасное тестирование Верификация конформности понимается как проверка соответствия исследуемой системы заданным требованиям. В модельном мире система отображается в реализационную модель (реализацию), требования — в спецификационную модель (спецификацию), а их соответствие — в бинарное отношение конформности. Если требования выражены в терминах взаимодействия системы с окружающим миром, возможно тестирование как проверка конформности в процессе тестовых экспериментов, когда тест подменяет собой окружение системы. Само отношение конформности и его тестирование основаны на той или иной модели взаимодействия.
Рис. 1. Машина тестирования. Мы будем рассматривать такие семантики взаимодействия, которые основаны только на внешнем, наблюдаемом поведении системы и не учитывают её внутреннее устройство, представленное на уровне модели в виде состояний. В этом случае говорят о тестировании методом «чёрного ящика» или функциональном тестировании. Мы можем наблюдать только такое поведение реализации, которое, во-первых, «спровоцировано» тестом (управление) и, вовторых, наблюдаемо во внешнем взаимодействии. Такое взаимодействие может моделироваться с помощью, так называемой, машины тестирования [13,15,16,22,23,33]. Она представляет собой «чёрный ящик», внутри которого находится реализация (Рис. 1). Управление сводится к тому, что оператор машины, выполняя тест (понимаемый как инструкция для оператора), нажимает какие-то кнопки на клавиатуре машины, «приказывая» или «разрешая» реализации выполнять те или иные действия, которые могут им наблюдаться. Наблюдения (на «дисплее» машины) бывают двух типов: наблюдение некоторого действия, разрешённого оператором и выполняемого 25
реализацией, и наблюдение отказа как отсутствия каких бы то ни было действий из числа тех, что разрешены нажатыми кнопками. Следует подчеркнуть, что при управлении оператор разрешает реализации выполнять именно множество действий, а не обязательно одно действие. Например, при тестировании реактивных систем, основанных на обмене стимулами и реакциями, посылка одного стимула из теста в реализацию может интерпретироваться как разрешение реализации выполнять только одно действие — приём этого стимула. Однако приём тестом ответной реакции должен означать разрешение реализации выдавать любую реакцию как раз для того, чтобы проверить, правильна эта реакция или нет. Мы будем считать, что оператор нажимает одну кнопку, но на кнопке «написано», вообще говоря, не одно действие, а множество разрешаемых действий. Когда происходит наблюдение (действие или отказ) кнопка автоматически отжимается, и все внешние действия считаются запрещёнными. Далее оператор может нажимать другую (или ту же самую) кнопку. В то же время «кнопочное» множество — это, вообще говоря, не любое подмножество множества всех действий. В вопросе о том, какие множества действий могут разрешаться тестом, а какие нет, среди исследователей существует большое разнообразие точек зрения. Например, для реактивных систем обычно считается, что нельзя (или бессмысленно) смешивать посылку стимулов с приёмом реакций (Ян Тритманс). Но существует и прямо противоположный подход: нельзя «тормозить» выдачу реакций реализацией, поэтому, даже посылая стимул, тест должен быть готов к приёму любой реакции (А. Ф. Петренко в [36]). Также следует подчеркнуть, что наблюдение отказа возможно не при любой кнопке. И здесь разные исследователи опираются на разные предположения. Для тех же реактивных систем долгое время считалось, что тест может наблюдать отсутствие реакций (quiescence, стационарность), например, по тайм-ауту, но не видит, принимает реализация посланный ей стимул или нет (input refusal, блокировка стимула). С другой стороны, в последние годы появляется всё больше и больше работ, в которых такие блокировки стимулов допускаются или допускаются частично [10-15,26,27,31]. Также и реакции, если они принимаются тестом по разным «выходным каналам», можно принимать не все, а лишь те, которые относятся к одному или нескольким выбранным каналам [26,27]. Итак, семантика взаимодействия определяется тем, какие (в принципе) существуют наблюдаемые действия — алфавит внешних действий L, какие множества действий может разрешать тест — набор кнопок машины тестирования, и для каких из этих кнопок наблюдаемы соответствующие отказы — семейство (L), а для каких нет — семейство (L). Предполагается, что = и =L. Такую семантику мы называем /-семантикой. 26
Кроме внешних, наблюдаемых действий реализация может совершать внутренние, ненаблюдаемые (и, следовательно, неразличимые оператором) действия, которые обозначаются символом . Выполнение таких действий не регулируется оператором — они всегда разрешены. Предполагается, что любая конечная последовательность любых действий совершается за конечное время, а бесконечная последовательность — за бесконечное время. Бесконечная последовательность -действий («зацикливание») называется дивергенцией и обозначается символом . Кроме этого мы вводим специальное, не регулируемое кнопками, действие, которое называем разрушением и обозначаем символом . Оно моделирует любое запрещённое или недекларированное поведение реализации. Например, в терминах пред- и постусловий, поведение программы определено (постусловием) только в том случае, когда выполнено предусловие обращения к ней. Если же предусловие нарушено, поведение программы считается полностью неопределённым. Семантика разрушения предполагает, в частности, что в результате такого поведения система может быть разрушена. При тестировании мы должны избегать возникновения ненаблюдаемых отказов (-отказов), попыток выхода из дивергенции и разрушения. Такое тестирование называется безопасным. Опасность разрушения подразумевается его семантикой. Поясним остальные случаи. В целом их опасность означает, что после нажатия кнопки оператор может не получить никакого наблюдения, и, не зная об этом, не может ни продолжать тестирование, ни закончить его. Если после нажатия кнопки P возникает -отказ P, оператор не знает, нужно ли ему продолжать ждать наблюдения внешнего действия, разрешённого нажатой кнопкой, или это бессмысленно, поскольку машина стоит. Однако, узнать об остановке машины оператор не может, поскольку это как раз означало бы наблюдение отказа P. Само по себе возникновение дивергенции не опасно, однако, нажимая после этого любую кнопку, оператор, не наблюдая внешнего действия или -отказа (если нажимается кнопка), не знает, случится ли такое наблюдение в будущем, или реализация так и будет бесконечно долго выполнять свои внутренние действия. Можно также отметить, что из-за внутренних действий нажатие пустой кнопки (кнопки с пустым множеством разрешаемых действий) не эквивалентно отсутствию нажатой кнопки. В обоих случаях все внешние действия запрещены, однако наблюдение отказа означает, что оператор узнаёт об остановке машины, когда она не может выполнять также и внутренние действия. Пустая -кнопка не может вызвать разрушение после действия (никакого действия быть не может), но она опасна, если есть дивергенция, как и любая другая кнопка. Пустую -кнопку вообще никогда нельзя нажимать, поскольку никакого наблюдения быть не может: все внешние действия запрещены, а отказ не наблюдаем. Поэтому можно считать, что .
27
2.2. LTS-модель и трассовая модель В качестве модели реализации и спецификации мы используем систему помеченных переходов (Labelled Transition System, LTS). LTS — это ориентированный граф с выделенной начальной вершиной, дуги которого помечены некоторыми символами. Формально, LTS — это совокупность S=LTS(VS,L,ES,s0), где VS — непустое множество состояний (вершин графа), L — алфавит внешних действий, — символ внутреннего действия, — символ разрешения, ESVS(L{,})VS — множество переходов (помеченных дуг графа), s0VS — начальное состояние (начальная вершина графа). Переход из состояния s в состояние s` по действию z обозначается szs`. Обозначим sz =def s` szs`. Выполнение LTS, помещённой в «чёрный ящик» машины тестирования, сводится к выполнению того или иного перехода, определённого в текущем состоянии и разрешаемого нажатой кнопкой (- и переходы разрешены при нажатии любой кнопки и при отсутствии нажатой кнопки). Состояние называется стабильным, если в нём нет - и -переходов, и дивергентным, если в нём начинается бесконечная цепочка -переходов (в частности, -цикл, в том числе, -петля). Отказ P порождается стабильным состоянием, в котором нет переходов по действиям из P. Для получения трасс LTS достаточно добавить в каждом стабильном состоянии виртуальные петли, помеченные порождаемыми состоянием отказами, а также добавить -переходы во всех дивергентных состояниях. После этого рассматриваются все конечные маршруты LTS, начинающиеся в начальном состоянии и не продолжающиеся после - или -перехода. Трассой маршрута считается последовательность пометок его переходов с пропуском -переходов. Такие трассы мы называем полными или F-трассами, а множество F-трасс LTS S — полной трассовой моделью или F-моделью, и обозначать F(S). F-трасса, все отказы которой принадлежат семейству , называется -трассой. Это те трассы, которые могут наблюдаться на машине тестирования в /-семантике. Множество всех -трасс LTS, то есть проекция её F-модели на алфавит, состоящий из всех внешних действий, -отказов, символов и , называется -моделью, соответствующей «взгляду» на реализацию в /-семантике.
2.3. Гипотеза о безопасности и безопасная конформность Безопасное тестирование, прежде всего, предполагает формальное определение на уровне модели отношения безопасности «кнопка безопасна в модели после -трассы». При безопасном тестировании будут нажиматься только безопасные кнопки. Это отношение различно для реализационной и спецификационной моделей. В LTS-реализации I отношение безопасности означает, что нажатие кнопки P после -трассы не может означать 28
попытку выхода из дивергенции (после трассы нет дивергенции), не может вызывать разрушение (после действия, разрешаемого кнопкой), и не может привести к ненаблюдаемому отказу (если это -кнопка): P safein I after =def uP u,F(I) & F(I). P safe in I after =def P safein I after & (P PF(I)). В LTS-спецификации S отношение безопасности отличается только для кнопок: мы не требуем, чтобы трасса не продолжалась безопасным отказом Q, но требуем, чтобы она продолжалась хотя бы одним действием zQ. Кроме того, если действие разрешается хотя бы одной неразрушающей -кнопкой, то оно должно разрешаться какой-нибудь безопасной кнопкой. Если это неразрушающая -кнопка, то она же и безопасна. Но если все неразрушающие кнопки, разрешающие действие, являются -кнопками, то хотя бы одна из них должна быть объявлена безопасной. Такое отношение безопасности всегда существует: достаточно объявить безопасной каждую неразрушающую кнопку, разрешающую действие, продолжающее трассу. Однако, в целом указанные требования неоднозначно определяют отношение safe by, и при задании спецификации указывается конкретное отношение. Требования к отношению safe by записываются так: R zL Q 1) R safe by S after R safein S after , 2) T T safein S after & zT & zF(S) P zP & P safe by S after , 3) Q safe by S after Q safein S after & vQ vF(S). Безопасность кнопок определяет безопасность действий и -отказов после трассы. -отказ R безопасен, если после трассы безопасна кнопка R. Действие безопасно, если оно разрешается некоторой кнопкой, безопасной после трассы. Теперь мы можем определить безопасные трассы. -трасса безопасна, если 1) модель не разрушается с самого начала (сразу после включения машины ещё до нажатия первой кнопки), то есть в ней нет трассы , 2) каждый символ трассы безопасен после непосредственно предшествующего ему префикса трассы. Множества безопасных трасс реализации I и спецификации S обозначим SafeIn(I) и SafeBy(S), соответственно. Требование безопасности тестирования выделяет класс безопасных реализаций, то есть таких, которые могут быть безопасно протестированы для проверки их конформности или неконформности заданной спецификации. Этот класс определяется следующей гипотезой о безопасности: реализация I безопасна для спецификации S, если 1) в реализации нет разрушения с самого начала, если этого нет в спецификации, 2) после общей безопасной трассы реализации и спецификации любая кнопка, безопасная в спецификации, безопасна после этой трассы в реализации: 29
I safe for S =def (F(S) F(I)) & SafeBy(S)SafeIn(I) P (P safe by S after P safe in I after ). Следует отметить, что гипотеза о безопасности не проверяема при тестировании и является его предусловием. После этого можно определить отношение (безопасной) конформности: реализация I безопасно конформна (или просто конформна) спецификации S, если она безопасна и выполнено тестируемое условие: любое наблюдение, возможное в реализации в ответ на нажатие безопасной (в спецификации) кнопки, разрешается спецификацией: I safe for S I saco S =def & SafeBy(S)SafeIn(I) P safe by S after obs(,P,I)obs(,P,S), — где obs(,P,T) =def {u|uF(T) & (uP u=P & P)} множество наблюдений, которые можно получить над моделью T при нажатии кнопки P после трассы .
2.4. Параллельная композиция и генерация тестов Взаимодействие двух систем моделируется в LTS-теории оператором параллельной композиции. Мы используем оператор композиции, аналогичный тому, который определяется в алгебре процессов CCS (Calculus of Communicating Systems) [32,34]. Будем считать, что для каждого внешнего действия z определено противоположное действие z так, что z=z. Например, посылке стимула из теста соответствует приём теста в реализации, а выдаче реакции реализацией соответствует приём этой реакции в тесте. Параллельное выполнение двух LTS в алфавитах A и B понимается так, что переходы по противоположным действиям z и z, где zA и zB, выполняются синхронно, то есть, в обеих LTS одновременно, причём в композиции это становится -переходом. Такие действия называются синхронными. Остальные внешние действия zA\B и zB\A, а также символы и называются асинхронными. Переход по такому символу выполняются в одной из LTS при сохранении состояния другой LTS. Результатом композиции двух LTS I и T становится LTS IT в алфавите AB =def (A\B)(B\A). Её состояния — это пары состояний it LTS-операндов, начальное состояние — это пара начальных состояний, а переходы порождаются следующими правилами вывода: (1) z{,}A\B & izi` itzi`t, (2) z{,}B\A & tzt` itzit`, iti`t`. (3) zAB & izi` & tzt` Тестирование понимается как замкнутая композиция LTS-реализации I в алфавите A и LTS-теста T в противоположном алфавите B=A. Для 30
обнаружения отказов в тесте (но не в реализации!) допускаются специальные -переходы, которые срабатывают тогда и только тогда, когда никакие другие переходы не могут выполняться: (4) tt` & Deadlock(i,t) itit`, где Deadlock(i,t) = i & i & t & t & (zAB iz tz). Мы будем предполагать, что в тесте также нет разрушения (требование t всегда выполнено). Поскольку алфавиты реализации и теста противоположны, композиционный алфавит пуст и в композиционной LTS есть только - и -переходы. При безопасном тестировании -переходы недостижимы. Выполнению теста соответствует прохождение -маршрута, начинающегося в начальном состоянии композиции IT. Тест заканчивается, когда достигается терминальное состояние теста. Каждому такому терминальному состоянию назначается вердикт pass или fail. Реализация проходит тест, если состояния теста с вердиктом fail недостижимы. Реализация проходит набор тестов, если она проходит каждый тест из набора. Набор тестов значимый, если каждая конформная реализация его проходит; исчерпывающий, если каждая неконформная реализация его не проходит; полный, если он значимый и исчерпывающий. Задача заключается в генерации полного набора тестов по спецификации. Обычно ограничиваются, так называемыми, управляемыми тестами, то есть тестами без лишнего недетерминизма. Для этого множество внешних действий, для которых определены переходы в данном состоянии теста, должно быть одним из «кнопочных» множеств /-семантики (точнее, множеством противоположных действий, поскольку при композиции CCS тест определяется в противоположном алфавите). Оператор, исполняя тест, однозначно определяет, какую кнопку ему нужно нажимать в данном состоянии теста. Если это -кнопка, то в состоянии должен быть также определён -переход. Полным набором всегда является набор всех примитивных тестов. Примитивный тест строится по одной выделенной безопасной -трассе спецификации. Для этого сначала в трассу перед каждым -отказом R вставляется кнопка R, а перед каждым действием z — какая-нибудь безопасная (после префикса трассы) кнопка P, разрешающая действие z. Для различения кнопок и отказов (и то и другое — подмножества внешних действий) мы будем кнопки заключать в кавычки и писать “P”, а не просто P. Безопасность трассы гарантирует безопасность кнопки R и наличие такой безопасной кнопки P. Выбор кнопки P может быть неоднозначным, то есть по одной безопасной трассе спецификации можно сгенерировать, вообще говоря, множество разных примитивных тестов. После расстановки кнопок 31
получается последовательность, которая во втором разделе статьи называется /-историей. По ней и строится LTS-тест (Рис. 2). Его состояниями становятся расставленные кнопки, начальное состояние — это первая в трассе кнопка, символы переходов из состояния-кнопки — это действия, противоположные тем, которые могут наблюдаться после нажатия этой кнопки, или символ , если это -кнопка. Если это не последняя кнопка, то один переход ведёт в состояние, соответствующее следующей кнопке. Остальные переходы ведут в терминальные состояния. Вердикт pass назначается тогда, когда соответствующая -трасса есть в спецификации, а вердикт fail — когда нет. Такой вердикт соответствует строгим тестам, которые, вопервых, значимые (не ловят ложных ошибок) и, во-вторых, не пропускают обнаруженных ошибок. Любой строгий тест можно заменить на объединение примитивных тестов, которое обнаруживает те же самые ошибки. =A,b,cвставляем ,безопасные кнопки “A”,A,“B”,b,“C”,c такие, что: A,B, C, bB, cC. Внешнее действие имеет индекс «хорошо», если оно есть в спецификации после префикса трассы; иначе — индекс «плохо». Пунктиром показаны взаимоисключающие переходы, ведущие из одного состояния. Индексы «хорошо» и «плохо» означают наличие или отсутствие продолжения префикса трассы соответствующим -отказом. Рис. 2. Примитивный тест для безопасной R-трассы . Теперь рассмотрим две проблемы, связанные с применением теории тестирования конформности на практике.
2.5. Недетерминизм и глобальное тестирование Как уже было сказано, в каждый момент времени реализация может выполнять любое определённое в ней и разрешённое оператором внешнее действие, а также определённые и всегда разрешённые внутренние действия (мы избегаем разрушения при безопасном тестировании). Если таких действий несколько, выбирается одно из них недетерминированным образом. Здесь предполагается, что недетерминизм поведения реализации — это явление того 32
уровня абстракции, которое определяется нашими тестовыми возможностями по наблюдению и управлению, то есть семантикой взаимодействия. Иными словами, поведение реализации недетерминировано, поскольку оно зависит от неких не учитываемых нами факторов — «погодных условий», которые определяют выбор выполняемого действия детерминировано. Для того, чтобы тестирование могло быть полным, мы должны предположить, что любые погодные условия могут быть воспроизведены в тестовом эксперименте, причём для каждого теста. Если такая возможность есть, тестирование называется глобальным [33]. Мы абстрагируемся от количества вариантов погодных условий. Здесь нам важна только потенциальная возможность проверить поведение системы при любых погодных условиях и любом поведении оператора. Конечно, на практике используется только конечное число прогонов каждого теста. Без дополнительных условий мы не можем быть уверены, что провели тестовые испытания каждого теста для всех возможных погодных условий. Возможны различные решения этой проблемы. Одно из них — специальные тестовые возможности по управлению погодой. Для этого мы должны выйти за рамки модели, которая как раз и абстрагировалась от второстепенных деталей внешних факторов, то есть от погоды. Тем самым, тестирование становится зависящим не только от спецификации, но и от реализационных деталей, от того, что можно назвать операционной обстановкой, в которой работает реализация. Для каждого варианта такой операционной обстановки мы будем вынуждены создавать свой набор тестов. Тем не менее, в некоторых частных случаях на этом пути можно получить практические выгоды. Другое решение — специальные реализационные гипотезы. Для конечного числа прогонов теста предполагают, что, если реализация ведёт себя правильно при некоторых погодных условиях, то она будет вести себя правильно при любых погодных условиях [6]. Третье решение основано на том, что нам известно распределение вероятностей тех или иных погодных условий. В этом случае тестирование оказывается полным с той или иной вероятностью [19]. Близкое к этому четвёртое решение предполагает, что в каждой ситуации (после трассы) возможно лишь конечное число погодных условий (с точностью до эквивалентности) и существует такое число N, что после N прогонов теста гарантированно будет проверено поведение реализации при всех возможных в этой ситуации погодных условиях [21,32]. Наконец, существует и более радикальное решение — просто запретить недетерминизм реализации, то есть реализационная гипотеза ограничивает класс реализаций только детерминированными реализациями. При всей своей наивности, это достаточно распространённый практический приём [35] (он применялся и в ИСП РАН в рамках системы UniTESK). Обоснованием может 33
служить то, что во многих случаях заранее известно, что интересующие нас реализации детерминированы.
2.6. Бесконечность полного набора тестов На практике используются только конечные наборы конечных (по времени выполнения) тестов. Поскольку тесты конечные, полный набор, как правило, содержит бесконечное число тестов (полный набор конечен только для моделей с конечным поведением, то есть конечным числом трасс; в частности, в LTS-спецификации не должно быть циклов). Возможны различные решения этой проблемы. В конечном счёте все они сводятся к специальным реализационным гипотезам. Такие гипотезы, по сути, предполагают ровно то, что хотелось бы «доказать»: если реализация ведёт себя правильно на тестах данного конечного набора, то она будет вести себя правильно на всех тестах полного набора. Обоснованием может служить некоторое (не важно как полученное) «знание» о том, как могут быть устроены тестируемые реализации (тестируем не все возможные реализации, а только такие). На классе всех реализаций такие конечные наборы тестов будут только значимыми (не ловят ложных ошибок), а полными — только на подклассе, определяемом реализационной гипотезой. Конечный набор тестов строится по тому или иному критерию покрытия, чтобы проверить все интересующие нас классы ситуаций (ошибок) [24,25,37]. Теоретически конечный набор можно получить фильтрацией по критерию покрытия перечислимого полного набора. Поэтому теория конформности должна быть развита до уровня алгоритмов перечисления полного набора. Однако на практике обычно используются более прямые методы построения нужного конечного набора. Достаточно общий подход сводится к тому, что вместо исходной спецификационной модели используется более грубая, так называемая, тестовая модель. Тестовая модель — это результат факторизации исходной LTS-спецификации по отношению эквивалентности переходов, что обычно сводится к эквивалентности состояний и/или действий [1]. Иногда при факторизации исчезает недетерминизм, что заодно решает и эту проблему. Разумеется, чтобы такой подход был оправданным, нужны мотивированные реализационные гипотезы о том, что ошибки, возможные в реализации, обнаруживаются при тестировании по факторизованной спецификации (вообще по конечному набору, удовлетворяющему критерию покрытия). Примером практического тестирования может служить тестирование конечного автомата по спецификации, заданной также в виде конечного автомата [3,4,6,18,28,29]. Если у нас есть специальная операция, позволяющая достоверно и напрямую опросить текущее состояние реализации (status message), то, как известно, полное тестирование сводится к обходу графа переходов автомата и применению операции опроса в каждом проходимом состоянии [3,6-9,20,29]. Обход графа переходов используется также в случае тестирования методом «чёрного ящика», когда состояния реализации не 34
видны. Но здесь для полноты тестирования требуются реализационные гипотезы, компенсирующие отсутствие тестовой возможности достоверного опроса состояний [3,4,6]. Предполагается, что число состояний реализации не превосходит число состояний спецификации (с точностью до эквивалентности состояний) или превосходит не более, чем на заранее известную величину [17,18,29,30]. Этот подход переносится и на общий случай LTS для реактивных систем [5]. В частности, когда используется, так называемое, стационарное тестирование, при котором стимулы подаются в реализацию только в её стационарных состояниях (в этом случае также снимается проблема торможения реакций) [2].
3. Теория конформности с приоритетами 3.1. Предикаты на переходах LTS-модели Независимо от наличия или отсутствия приоритетов семантика взаимодействия предполагает, что выполняться может только то действие, которое определено в реализации и разрешено оператором машины тестирования. Если приоритетов нет, то выполняться может любое определённое и разрешённое действие, и выбор выполняемого действия не детерминирован. Наличие приоритетов означает, что не все определённые и разрешённые действия могут выполняться, то есть выполнимость действия зависит также от того, какие ещё действия определены и/или разрешены. Эту зависимость можно изобразить в виде предиката от множества разрешённых действий, который назначается переходу LTS-модели. Поскольку для перехода szs` известно его пресостояние s, а для этого состояния s известно, какие ещё переходы в нём начинаются, предикат на переходе можно считать не зависящим от множества определённых (в состоянии s) действий. Иными словами, переходы по одному и тому же действию, ведущие из разных состояний, могут иметь разные предикаты. LTS-модель с приоритетами — это LTS, алфавит которой — декартовое произведение алфавита внешних действий и множества предикатов на алфавите внешних действий ={:(L)Bool}: S=LTS(VS,L,ES,s0). Переход sz,s` может выполняться только тогда, когда оператор разрешает такое множество внешних действий RL, что zR{,} и (R)=true. Если есть несколько выполнимых действий, выполняется одно из них, выбираемое, по-прежнему, недетерминированным образом. Предикат можно понимать как булевскую функцию от булевских переменных z1,z2,…, взаимно-однозначно соответствующих внешним действиям из алфавита L. Например, для предиката =a&bc переход sz,s` может выполняться только тогда, когда оператор разрешил такое множество внешних действий R, что zR{,} & (aR&bRcR). Это означает, 35
что действие z — это внутреннее действие, разрушение или внешнее действие, разрешённое оператором, а также выполнено хотя бы одно из двух условий: 1) оператор разрешил действие a и не разрешил действие b, 2) оператор разрешил действие c. Итак, в общем случае предикат — это булевская функция от множества разрешённых действий. Можно отметить важный частный случай, когда предикат зависит только от разрешённых и определённых внешних действий. Иными словами, предикат на переходе sz,s` не зависит от тех булевских переменных, которые соответствуют внешним действиям, по которым нет переходов из состояния s. Это означает, что выполнимость перехода зависит только от того, разрешено ли действие z, и какие ещё действия определены в состоянии s и разрешены оператором. В этом случае реализацию не интересуют (она «не видит») те действия, которые оператор разрешает, но они всё равно не могут выполняться, поскольку не определены в текущем состоянии реализации. Нажимая кнопку, оператор как бы «подсвечивает» некоторые действия реализации, определённые в её текущем состоянии, и выполнимость перехода по каждому из них определяется соответствующим предикатом от множества «подсвеченных» действий. Предикат как булевская функция от булевских переменных-действий может быть представлен в виде совершенной дизъюнктивной нормальной формы (СДНФ) =12…, где i=xi1&xi2&…, xij=zj или xij=zj, и zj пробегает множество всех внешних действий. Тогда переход sz,s` можно заменить на множество кратных переходов с предикатамидизъюнктами sz,is`. В свою очередь дизъюнкту i взаимнооднозначно соответствует множество Pi тех действий, для которых xij=zj. При композиции это множество является множеством действий, разрешаемых партнёром. В случае, когда таким партнёром является тест для заданной /семантики, эти множества разрешаемых действий соответствуют кнопкам из . Тем самым, при тестировании мы можем считать, что на переходах написаны не произвольные предикаты, а кнопки sz,Pis`. Когда нажимается некоторая кнопка Pi, выполняться могут только переходы вида sz,Pis`, где zPi{,}. Такой переход от LTS с предикатами к LTS с кнопками аналогичен переходу от расширенных автоматов (EFSM — Extended Finite State Machine) к обычным автоматам (FSM).
3.2. Остановка и дивергенция Машина без приоритетов останавливается в стабильном состоянии (состоянии без - и -переходов), в котором нет переходов по разрешённым внешним действиям. Если есть приоритеты, то меняется, прежде всего, само понятие стабильности. Оно становится условным: состояние стабильно, если для всех переходов из этого состояния их предикаты от пустого множества разрешённых внешних действий ложны ()=false. Соответственно 36
меняется условие остановки машины: машина стоит, если при разрешённом множестве внешних действий P для всех переходов из этого состояния их предикаты ложны (P)=false. Здесь мы должны уточнить, что происходит, когда кнопка отжимается. Для машины без приоритетов кнопка автоматически отжимается при любом наблюдении действия или отказа. После действия машина может выполнять любые -переходы (а также -переход), но после отказа машина стоит, поскольку отказ происходит в стабильном состоянии, в котором нет - и переходов. Однако для машины с приоритетами отжатие кнопки меняет множество разрешённых действий (если только не была нажатой пустая кнопка, дающее единственное наблюдение пустого отказа). После наблюдения реализация начинает выполнять -переходы с приоритетом ()=true. Заметим, что таким наблюдением может быть не только действие, но и отказ. Причина этого в том, что отказ P означал невозможность выполнения разрешённых внешних действий zP, а также - и -переходов, поскольку их предикаты стали ложны (P)=false. После отжатия кнопки P множество разрешённых внешних действий пусто и теперь могут выполняться - и переходы с предикатами ()=true. Далее оператор может снова нажать ту же кнопку (но без гарантии повторного наблюдения того же отказа, если реализация сменила состояние по -переходам) или другую кнопку. Если допускается переключение кнопок, то есть нажатие второй кнопки, не дожидаясь наблюдения по первой кнопке, то это интерпретируется как отжатие первой кнопки, а потом нажатие второй кнопки. Мы будем считать, что «в промежутке» между двумя кнопками создаётся ситуация, когда ни одна кнопка не нажата, и реализация может выполнять - и -переходы с предикатами ()=true. Общая парадигма здесь заключается в том, что ситуация отсутствия тестового воздействия возникает всегда при включении машины (до нажатия первой кнопки), после любого наблюдения и между двумя тестовыми воздействиями. Более подробно переключение кнопок рассматривается в следующем подразделе. Мы уже говорили, что даже для машины без приоритетов проблема дивергенции не в ней самой по себе, а в выходе из неё. При наличии приоритетов, если внешнее воздействие имеет больший приоритет, чем внутренняя активность, дивергенция прекращается. Теперь выполнимость действий зависит от нажатой кнопки, и мы можем косвенно управлять ими и, следовательно, дивергенцией. Тогда можно говорить о выполнимой дивергенции: при одной нажатой кнопке (или когда нет нажатой кнопки) все -действия бесконечной цепочки выполнимы, а при другой — нет и, следовательно, нет «зацикливания». Выйти из дивергенции, которая начинает выполняться после кнопки A, можно с помощью кнопки B, при которой дивергенция не выполнима. Заметим, что для этого требуется переключение кнопок, то есть нажатие кнопки без наблюдения (которого может не быть). 37
Единственный случай, когда из дивергенции нельзя гарантированно выйти, — это когда дивергенция выполнима при нажатии любой кнопки.
3.3. Переключение кнопок В машине без приоритетов кнопку можно нажимать либо после включения машины, либо после того, как произошло наблюдение по предыдущей кнопке. Иными словами, запрещается переключать кнопки без наблюдения, отжимая одну кнопку и нажимая другую. Этот запрет объясняется тем, что, если приоритетов нет, возможность переключения кнопок не увеличивает мощность тестирования. Действительно, если была нажата кнопка P, а потом без наблюдения нажата другая кнопка Q (а кнопка P отжата), то в этом интервале времени реализация могла выполнять только -действия. Но действия всегда разрешены, поэтому реализация могла бы выполнять их и в том случае, когда вместо кнопки P сразу нажималась кнопка Q (а второй раз, естественно, не нажималась). Тем самым, любое поведение, которое можно наблюдать в первом случае, можно было бы наблюдать и во втором случае.
Рис. 3. Переключение кнопок. При наличии приоритетов переключение без наблюдения необходимо для полноты тестирования, поскольку различные множества разрешённых действий по-разному влияют на выполнение -действий (-переходы тоже могут иметь предикаты), что приводит к внешне различимым поведениям. Например, если в реактивной системе приём стимула приоритетнее выдачи реакций и -действий, последние выполняются только тогда, когда реализация не может принять стимул, посылаемый ей тестом. На Рис. 3 показан пример, где для получения тестом реакции !y после стимула ?a нельзя сразу посылать этот стимул (тогда после него будет реакция !x), а нужно сначала послать стимул ?b, а потом переключить кнопку {?b} на кнопку {?a}, послав тем самым стимул ?a. Если реализация принимает стимул ?b, то переключение нужно успеть сделать до приёма стимула ?b. Если же реализация блокирует стимул ?b (нет пунктирного перехода), то можно «не торопиться»; если блокировка {?b} наблюдаема, можно сначала её дождаться, а потом послать стимул ?a. 38
Тем не менее, в пользу запрета на переключение кнопок имеются разумные аргументы и в случае наличия приоритетов. Дело в том, что переключение кнопок позволяет «обходить» ненаблюдаемый отказ: если оператор переключает -кнопку P на (любую) другую кнопку Q, то возникновение ненаблюдаемого отказа P не препятствует такому переключению (как на 0, когда нет пунктирного перехода по стимулу ?b, а блокировка {?b} не наблюдаема). Другое дело, что, если возможен отказ P, то при безопасном тестировании мы не можем нажимать кнопку P без последующего переключения на другую кнопку, то есть с ожиданием наблюдения (на 0 кнопку {?b} всегда нужно переключать на кнопку {?a} или какую-то другую). Тем самым, если можно переключать кнопки, условие безопасности -кнопок более сложное (ниже мы его подробно рассмотрим). Если переключений кнопок нет, тестирование выглядит более привычно как чередующаяся последовательность тестовых воздействий (нажимается кнопка) и наблюдений. Кроме того, в этом случае к работе оператора предъявляется меньше временных требований.
3.4. Временные ограничения на работу оператора (теста) Введение приоритетов усложняет работу оператора, налагая более сложные требования по времени. Если приоритетов нет, то оператор должен уметь достаточно быстро нажимать кнопку после включения машины или после предыдущего наблюдения. Заметим, что если оператор не успевает достаточно быстро нажать кнопку, ничего страшного не случится, поскольку машина успеет выполнить только одно или несколько -действий, которые (в машине без приоритетов) она может выполнить и в том случае, когда кнопка была нажата немедленно. Иными словами, мы требуем, чтобы оператор мог работать быстро, но не заставляем его всегда работать быстро. Если приоритеты есть, то возможность наблюдения тех или иных поведений реализации требует не только достаточно высокой скорости работы оператора, но также достаточно медленной, средней и т.д. В примере на Рис. 4 стимул ?a может приниматься в трёх состояниях 1, 2 и 3, но реакции после этого различны: !x, !y или !z. Эти состояния связаны -переходами, которые выполнимы только, если тест не посылает стимул ?a. Поэтому реакция !x будет наблюдаться только, если оператор быстро нажмёт кнопку {?a}, реакция !z — только если оператор не будет торопиться, а реакция !y — только при средней скорости работы. Если есть переключение кнопок, то такое переключение также нужно уметь делать с различными интервалами времени, чтобы «заставить» реализацию проходить нужное число -переходов между двумя кнопками.
39
Рис. 4. Модель, поведение которой зависит от скорости нажатия кнопок. Таким образом, для машины с приоритетами следует учитывать временные задержки, которые делает оператор между наблюдением и последующим нажатием кнопки или между двумя нажатиями кнопок при их переключении без наблюдения. Мы можем считать, что в погодные условия включены также те факторы, которые влияют на «свободу воли» оператора, определяя те или иные временные задержки при нажатии кнопок. Это согласуется с тем, что оператор должен моделировать любую скорость работы окружения. Работа оператора моделирует выполнение тестовой программы на компьютере. Такая программа недетерминирована только на некотором уровне абстракции, когда мы отвлекаемся от других программ или аппаратуры, влияющих на её поведение.
3.5. Истории Если приоритетов нет, возможность наблюдения действия после некоторой предыстории взаимодействия не зависит от того, какая именно нажимается кнопка, разрешающая это действие. При наличии приоритетов это становится важным, поскольку различным кнопкам соответствуют различные множества разрешённых действий, и при нажатии одной кнопки предикат может оказаться истинным, и действие может наблюдаться, а при нажатии другой — ложным, и действие не может наблюдаться. Поэтому теперь нужно запоминать не только наблюдения, но также те кнопки, которые нажимал оператор. Тем самым, результатом тестового эксперимента становится последовательность действий, отказов и кнопок. Такую последовательность мы будем называть историей. Чтобы в истории отличить кнопку от отказа (и то и другое — подмножество внешних действий), мы будем кнопку заключать в кавычки и писать “P”, а не просто P. Если не ограничиваться только безопасным тестированием, то мы должны включить в истории также разрушение и дивергенцию, но после них история не может продолжаться, аналогично трассам. Очевидно, что в истории каждому внешнему действию z непосредственно предшествует кнопка “P”, разрешающая это действие zP, а каждому отказу R — -кнопка “R”. Могут или не могут идти две кнопки подряд, зависит от того, разрешено или запрещено переключение кнопок. 40
Для заданной /-семантики истории будем называть /-историями. Определим их более формально. Рассмотрим LTS с предикатами S. Для множества разрешённых действий P переход sz,s` будем называть P-выполнимым, если его предикат истинен (P)=true. Будем говорить, что для множества разрешённых действий P -маршрут P-выполним, если все его переходы P-выполнимы. Пустая /-история заканчивается в состояниях, достижимых из начального состояния по -выполнимым -маршрутам, то есть, когда после включения машины не нажата никакая кнопка. Пусть /-история заканчивается во множестве состояний S after . Рассмотрим различные продолжения этой /-истории. Мы будем предполагать, что /-история не заканчивается разрушением или дивергенцией, поскольку после дивергенции и разрушения нет продолжений. Продолжение кнопкой P, где P. Если допускается переключение кнопок, такое продолжение всегда возможно. Если переключения кнопок нет, /-история не должна заканчиваться кнопкой. Переключение интерпретируется как отжатие первой кнопки, а потом нажатие второй кнопки. Поэтому сначала реализация может выполнить любой -выполнимый -маршрут, начинающийся в состоянии из S after , а затем продолжить выполнение любым P-выполнимым -маршрутом. Множество концов таких маршрутов и будет множеством состояний S after “P”. Заметим, что, если история не заканчивалась на кнопку, то концы всех -выполнимых маршрутов уже входят в S after . Продолжение внешним действием z. Такое продолжение возможно только в том случае, когда сама /-история имеет вид “P”, то есть заканчивается кнопкой P, разрешающей это действие zP. Наблюдение действия z происходит, когда совершается P-выполнимый переход по z из состояния после предшествующей /-истории, то есть перехода sz,s`, где s(S after “P”) и (P)=true. В результате такого перехода кнопка автоматически отжимается, и далее могут выполняться -выполнимые маршруты до тех пор, пока не возникнет разрушение, пока не будет нажата кнопка (та же самая или другая), или пока оператор не выключит машину, заканчивая сеанс тестирования. Множество концов этих -маршрутов и является множеством S after “P”,z. Продолжение -отказом P. Такое продолжение возможно только в том случае, когда сама /-история имеет вид “P”. Отказ P возникает в таком состоянии s(S after “P”), в котором выполнено условие остановки машины: для каждого перехода (по любому действию, включая и ) sz,s` должно быть (P)=false. После отказа кнопка 41
отжимается и реализация может выполнить -выполнимый -маршрут, начинающийся в одном из состояний, где наблюдался отказ. Множество концов этих -маршрутов и является множеством S after “P”,P.Заметим, что состояния, где наблюдался отказ, тоже входят в это множество (для пустого -маршрута). Продолжение разрушением . Такое продолжение возможно только в том случае, когда в некотором состоянии s(S after ) переход s,s` P-выполнимым, если /-история заканчивается кнопкой P (наблюдения ещё не было, и продолжает действовать кнопка P), или -выполним в противном случае (после наблюдения не действует никакая кнопка). Поскольку после разрушения нет продолжения, нас не интересует множество состояний после такого продолжения. Продолжение дивергенцией . Поскольку опасна не сама дивергенция, а попытка выхода из неё, нас будет интересовать только такая дивергенция, которая выполнима при нажатой кнопке P. Такая дивергенция возникает после /-истории вида “P”, если есть бесконечный P-выполнимый маршрут, начинающийся в состоянии из S after “P” (очевидно, достаточно считать, что маршрут начинается в состоянии из S after ). В этом случае символ будет продолжать /-историю после кнопки P. Поскольку после дивергенции нет продолжения, нас не интересует множество состояний после такого продолжения. Теперь аналогично трассам определим полные истории или F-истории как /-истории для =(L) и, соответственно, =, когда любое подмножество внешних действий является -кнопкой. Множество F-историй LTS S — обозначим так же, как множество F-трасс, — F(S), поскольку в дальнейшем мы будем рассматривать только истории, а не трассы. Теперь /-история LTS — это такая её F-история, в которой встречаются кнопки только из семейств и , а отказы — это только -отказы.
3.6. Безопасность и конформность без переключения кнопок Поскольку выполнимость переходов LTS-модели с приоритетами зависит от предикатов на этих переходах, меняются отношения безопасности кнопок в реализации (safe in) и спецификации (safe by). Если нет переключения кнопок, то отношения safe in и safe by определяются почти так же, как для машины без приоритетов. Изменения заключаются в том, что вместо -трасс рассматриваются /-истории, безопасность или опасность кнопки определяется только после /-истории, не заканчивающейся кнопкой, продолжение внешним действием зависит от кнопки, дивергенция возможна лишь после кнопки, разрушения не должно быть не только после действия, но также после отказа (для -кнопки) и сразу после нажатия кнопки. 42
Определение отношения безопасности в реализации без переключения кнопок: P safein I after =def “P”,F(I) & uP “P”,u,F(I) & (P “P”,P,F(I)). P safein I after =def P safein I after & “P”,F(I). P safe1in I after =def P safein I after &(P“P”,PF(I)). Требования к отношению безопасности в спецификации без переключения кнопок: R zL Q 1) R safe1by S after R safein S after , 2) T T safein S after & “T”,zF(S) P P safe1by S after & “P”,zF(S), 3) Q safe1by S after Q safein S after &vQ “Q”,vF(S). На основе отношений безопасности кнопок в реализации и спецификации определяются безопасные действия, безопасные /-истории Safe1In(I) и Safe1By(S), гипотеза о безопасности и безопасная конформность аналогично тому, как это делалось для трасс в случае машины без приоритетов. Отличия сводятся к следующему: В определении безопасности /-истории и в гипотезе о безопасности следует говорить не о трассе , а об истории , то есть о разрушении, выполнимом без нажатия кнопок (-выполнимом). I safe for S =def (“”,F(S) “”,F(I)) & Safe1By(S)Safe1In(I) P (P safe1by S after P safe1in I after ). В определении множества наблюдений, которые можно получить над моделью T при нажатии кнопки P после трассы , вместо продолжения трассы наблюдением нужно говорить о продолжении /-истории кнопкой и наблюдением: obs(,P,T) =def {u|“P”,uF(T) & (uP u=P & P)}. I saco S =def I safe for S & Safe1By(S)Safe1In(I) P safe1by S after obs(,P,I)obs(,P,S).
3.7. Безопасность и конформность с переключением кнопок Если допускается переключение кнопок, мы можем обходить запрет на возникновение ненаблюдаемого отказа после нажатия -кнопки Q, а также дивергенцию, просто переключая её на другую кнопку P. Соответственно, модифицируются отношения безопасности: удаляются условия в определениях safe1in и safe1by, подчёркнутые волнистой линией, и остаются условия, связанные только с разрушением. 43
Определение отношения безопасности в реализации с переключением кнопок: P safe2in I after =def P safein I after . Требования к отношению безопасности в спецификации с переключением кнопок: R zL Q 1) R safe2by S after R safein S after , 2) T T safein S after & “T”,zF(S) P P safe2by S after & “P”,zF(S), 3) Q safe2by S after Q safein S after . Однако возникает вопрос: сколько раз оператор может переключать кнопки? Здесь нужно учитывать, что целью нажатия кнопок является получение, в конечном счёте, некоторого наблюдения. Поскольку мы рассматриваем только конечные (по времени выполнения) тесты, цепочка переключений кнопок должна быть конечной, то есть заканчиваться нажатием кнопки, после которой оператор ожидает гарантированного наблюдения, что даёт ему возможность, в частности, закончить сеанс тестирования. Это означает, что все кнопки в цепочке, кроме последней, безопасны после непосредственно предшествующего им префикса истории по отношению safe2in/by, а последняя кнопка — по отношению safe1in/by: P safe in I after =def P0=P,P1,…,Pn & i=0..n-1 Pi safe2in I after “P0”,“P1”,…,“Pi-1” & Pn safe1in I after “P0”,“P1”,…,“Pn-1”, P safe by S after =def P0=P,P1,…,Pn & i=0..n-1 Pi safe2by S after “P0”,“P1”,…,“Pi-1” & Pn safe1by S after “P0”,“P1”,…,“Pn-1”. Таким образом, отношение безопасности с индексом “2” определяет продолжение истории кнопкой, не вызывающей разрушение, а отношение безопасности с индексом “1” дополнительно запрещает ненаблюдаемый отказ и дивергенцию. Понятно, что 2-безопасная кнопка также и 1-безопасна, но обратное, вообще говоря, не верно. Для полной безопасности кнопки после истории требуется, чтобы она была 1-безопасна, и после неё можно было поместить конечную цепочку 1-безопасных кнопок, а затем 2-безопасную кнопку, гарантирующую наблюдение. На основе отношений безопасности кнопок в реализации и спецификации определяются безопасные действия, безопасные истории, гипотеза о безопасности и безопасная конформность аналогично тому, как это делалось для случая без переключения кнопок. Отличия сводятся к следующему: 1) В гипотезе о безопасности из i-безопасности кнопки в спецификации должна следовать тоже i-безопасность кнопки в реализации, где i=1,2: 44
I safe for S =def (“”,F(S) “”,F(I)) & SafeBy(S)SafeIn(I) P (P safei by S after P safei in I after ). 2) В определении конформности вложенность множеств наблюдений должна требоваться только после 1-безопасных историй, то есть историй, заканчивающихся на 1-безопасную кнопку: I safe for S I saco S =def & SafeBy(S)SafeIn(I) P safe1by S after obs(,P,I)obs(,P,S).
3.8. Параллельная композиция и генерация тестов Рассмотрим композицию двух LTS с приоритетами I и T в алфавитах, соответственно, A и B. Возьмём любое композиционное состояние it. При композиции множество разрешённых внешних действий для LTS I в состоянии i — это множество противоположных внешних действий, по которым есть переходы из состояния t другой LTS T, и наоборот. Поэтому, прежде всего, нам нужно пересчитать предикаты переходов из этих состояний. В силу коммутативности оператора композиции (с точностью до изоморфизма, то есть именования состояний it или ti), нам достаточно рассмотреть только пересчёт предикатов одной LTS, для определённости, LTS I. Для перехода iz,ii` мы должны в предикат i, понимаемый как булевская функция от булевских переменных-действий, подставить константное значение каждой переменной, соответствующей синхронному действию zAB. Если есть переход tz,tt`, то подставляется значение true, иначе — false. Получается новый предикат it. Заметим, что вычисление нового предиката на переходе из состояния i зависит от состояния t, с которым оно компонуется, то есть для разных состояний t будут разные предикаты it. Новый предикат it может быть не константным, поскольку в нём могут остаться переменные, соответствующие асинхронным внешним действиям из A\B. Кроме того, теперь этот предикат следует понимать как предикат в композиционном алфавите AB=(A\B)(B\A), хотя реально он не зависит от переменных, соответствующих действиям из B\A. Асинхронный переход соответствует одному переходу в одном из LTSоперандов. Он может выполняться, если может выполняться наследуемый переход. Следовательно, предикат асинхронного композиционного перехода совпадает с предикатом наследуемого перехода после пересчёта, то есть не с исходным предикатом i, а с предикатом it. Синхронный переход — это одновременное выполнение переходов в каждом LTS-операнде. Он может выполняться, если могут выполняться оба перехода-операнда. Следовательно, 45
предикат синхронного композиционного перехода равен конъюнкции пересчитанных переходов-операндов it&ti. В целом композиционные переходы порождаются следующими правилами вывода: (1*) z{,}A\B & iz,ii` itz,iti`t, tz,tt` itz,tiit`, (2*) z{,}B\A & (3*) zAB & iz,ii` & tz,tt` it,it&tii`t`. Как и в случае машины без приоритетов тестирование понимается как композиция LTS-реализации I в алфавите A и LTS-теста T в противоположном алфавите B=A. Мы также будем предполагать, что в тесте нет разрушения. Переходы по внешним действиям в тесте не имеют предикатов, точнее их предикаты тождественно истинны. Поэтому в композиционной LTS все переходы (а это уже только - и -переходы) — это пересчитанные предикаты переходов реализации. Поскольку композиционный алфавит пуст, эти предикаты константны (true или false). Для обнаружения отказов в тесте (но не в реализации!) также используются переходы с тождественно истинными переходами. Если нет переключения кнопок, такой переход срабатывает тогда и только тогда, когда никакие другие переходы не могут выполняться: (4*) tt` & Deadlock(i,t) itit`, где Deadlock(i,t) = i,it & i,it & t & (zAB iz,it tz). Если допускается переключение кнопок, то в тесте оно отображается в виде перехода “P”“Q” из состояния, соответствующего одной кнопке P, в состояние, соответствующее другой кнопке Q. -переход определяется в состоянии “P”, если кнопка P — это -кнопка. Нужно, чтобы -переход мог срабатывать независимо от -перехода переключения кнопок “P”“Q”: удаляется условие t, подчёркнутое волнистой линией. Мы будем рассматривать только безопасные реализации и безопасные тесты. Под безопасным тестом здесь понимается тест, при взаимодействии которого с любой безопасной реализацией не возникают разрушение, дивергенция, выполнимая после нажатия кнопки, а также тупики. Такие тесты строятся на основе безопасных историй спецификации. Тестовая история — это либо безопасная история спецификации, либо безопасная история спецификации, заканчивающаяся кнопкой, которая продолжена наблюдением (действием или -отказом), отсутствующим после этой истории в спецификации. Также мы будем рассматривать только такие тесты, которые заканчиваются через конечное время; мы будем называть их конечными тестами. Для безопасного LTS-теста это означает отсутствие в нём бесконечных маршрутов.
46
=“A”,A,“B”,b,“C”,c, где A,B, C. Внешнее действие имеет индекс «хорошо», если оно есть в спецификации после префикса истории; иначе — индекс «плохо». Пунктиром показаны взаимоисключающие переходы, ведущие из одного состояния. Индексы «хорошо» и «плохо» означают наличие или отсутствие продолжения префикса истории соответствующим -отказом. Маленькими серыми кружками показаны пустые состояния. Рис. 5. Примитивный тест для безопасной R/Q-истории . В композиции теста с реализацией все предикаты константны, мы можем удалить все переходы с ложными предикатами. Если реализация безопасна, а тест конечен и безопасен, то оставшиеся -переходы недостижимы. Как и для машины без приоритетов выполнению теста соответствует прохождение маршрута, начинающегося в начальном состоянии композиции и заканчивающегося в композиционном состоянии it, где t терминальное состояние теста, которому назначен вердикт pass или fail. Заметим, что в композиции могут быть бесконечные маршруты, но они не могут проходиться при тестировании. Действительно, поскольку тест конечен, в таком маршруте, начиная с некоторого места, состояние теста не меняется: дальше идут только асинхронные -переходы реализации. В процессе тестирования оператор машины всегда за конечное время дожидается наблюдения, нажимает или переключает кнопку, что означает смену состояния теста и, тем самым, прекращение выполнения бесконечной цепочки -переходов реализации. Здесь мы опираемся на то, что за конечное время может выполняться только конечная цепочка переходов. Оператор может также выключить машину (окончание тестирования), что происходит через конечное время после наблюдения. Тест должен взаимодействовать с реализацией, согласно той /-семантики, в которой рассматривалась спецификация, по которой тест сгенерирован. 47
Поэтому в каждом состоянии теста множество действий, по которым определены переходы из состояния, должно соответствовать - или -кнопке, причём для -кнопки дополнительно должен быть определён -переход. Мы приняли допущение о том, что сразу после включения машины до нажатия первой кнопки, после любого наблюдения, а также при переключении кнопок в реализации могут выполняться -выполнимые - и -переходы. Это допущение является частью семантики взаимодействия, которую должен соблюдать тест. Поэтому нужно ввести в тест дополнительные пустые состояния, соответствующие ситуации, когда нет нажатых кнопок. Эти состояния теста должны позволять реализации совершать -выполнимые переходы. При отжатых кнопках множество разрешённых действий пусто, поэтому в пустом состоянии могут быть определены только -переходы. Эти -переходы, в конечном счёте, должны приводить к непустым состояниям, соответствующим той или иной кнопке (пустая -кнопка соответствует состоянию, в котором определён -переход, а пустую -кнопку мы запретили). Такой пустой кнопкой должно быть начальное состояние и постсостояние каждого перехода по наблюдению, если это не терминальное состояние. Примитивный тест строится аналогично тому, как это делается для машины без приоритетов. Но есть три отличия. 1) Без приоритетов мы строили тест по безопасной -трассе, превращая её в одну из /-историй, а теперь сразу начинаем с некоторой безопасной /-истории. 2) Если в истории есть переключение с кнопки P на кнопку Q, то в тесте проводится -переход “P”“Q”. 3) Добавляются пустые состояния. По-прежнему, набор всех примитивных тестов полон, а любой строгий тест можно заменить на объединение примитивных тестов, которое обнаруживает те же самые ошибки.
3.9. Примеры задания приоритетов Покажем, как задаются приоритеты с помощью предикатов на переходах LTSмодели для примеров, приведённых во введении. Выход из дивергенции. Переход по внешнему действию имеет тождественно истинный предикат, а -переход имеет предикат , истинный только на пустом подмножестве алфавита внешних действий: (U)=(U=). Выход из осцилляции (приоритет приёма над выдачей). Переход по стимулу имеет тождественно истинный предикат, а переход по реакции имеет предикат , истинный на любом подмножестве действий, не содержащем стимулов: (U)=(?x ?xU). Обычно также подразумевается, что внутренняя активность менее приоритетна, чем приём стимула, то есть -переход имеет такой же предикат, как переход по реакции. Приоритет выдачи над приёмом в неограниченных очередях. Переход по реакции имеет тождественно истинный предикат, а переход по стимулу имеет 48
предикат , истинный на любом подмножестве действий, не содержащем реакцию: (U)=(!y !yU). Обычно также подразумевается, что внутренняя активность менее приоритетна, чем выдача реакции, то есть переход имеет такой же предикат, как переход по стимулу. Прерывание цепочки действий. Переход по команде «отменить» (cancel) имеет тождественно истинный предикат, а все остальные переходы имеют предикат , истинный на любом подмножестве действий, не содержащем “cancel”: (U)=(cancelU). Приоритетная обработка входных воздействий. Множество стимулов разбивается на непересекающиеся подмножества X1,X2,… так, что стимулы из подмножества с большим индексом имеют больший приоритет. Предикат i на переходе по стимулу из Xi истинен на любом подмножестве действий, не содержащем стимулы из подмножества с большим номером: i(U)=(j>i UXj=). Возможна также дифференциация переходов из некоторого состояния по одному и тому же стимулу в зависимости от наличия или отсутствия менее приоритетных стимулов. Например, один переход по стимулу из Xi выполняется, если окружение предлагает менее приоритетные стимулы i1(U)=i(U)&(j
4. Заключение Можно рассматривать семантики, в которых при включении машины, после наблюдения и при переключении кнопок может не допускаться выполнение реализацией - и -переходов, даже если они -выполнимы. Можно считать, что сразу после включения машины и сразу после наблюдения реализация стоит, и может выполнять какие-то действия только после нажатия кнопки. Также переключение кнопок не интерпретируется как отжатие первой кнопки (с разрешением -выполнимых - и -действий), а потом нажатие второй 49
кнопки. Иными словами, после включения машины, после наблюдения и между двумя кнопками при переключении кнопок нет никакого «пустого» промежутка. Такая семантика, очевидно, предполагает более сильные тестовые возможности, чем слабая семантика, рассматриваемая в данной статье. Эти семантики имеют разные требования по безопасности и конформности. Любое поведение, которое можно наблюдать при сильной семантике, можно наблюдать и при слабой семантике. Достаточно подобрать подходящие погодные условия, когда оператор успевает нажать или переключить кнопку достаточно быстро. Верно и обратное: поведение при слабой семантике наблюдается при сильной семантике, если добавить пустую кнопку и явно нажимать её. Однако условия безопасности для этих семантик разные. При слабой семантике мы всегда должны рассчитывать на возможность выполнения - и -действий (при наличии приоритетов, они должны быть выполнимы) после наблюдения по кнопке P, а такие действия могут давать дивергенцию или разрушение; тем самым, кнопка P будет опасной. При сильной семантике мы можем просто не нажимать в этой ситуации пустую кнопку после такого наблюдения, поскольку она опасна, а кнопка P будет безопасной. Отсюда же вытекают и различия в конформности: реализация может быть опасной при слабой семантике и, следовательно, не конформной, но безопасной и конформной при сильной семантике. При тех же условиях безопасности (например, когда в спецификации нет дивергенции, разрушения и ненаблюдаемых отказов) и при наличии приоритетов сильная семантика предъявляет более жёсткие условия конформности. Это объясняется тем, что мы получаем возможность различать реализации, в которых некое действие b, разрешаемой кнопкой B, выполняется сразу после действия a или через промежуточную -выполнимую, но не B-выполнимую -активность. Кроме генерации тестов, важнейшей проблемой теории конформности является проблема монотонности — сохранения конформности при композиции. В общем случае композиция реализаций, конформных своим спецификациям, может быть не конформна композиции этих спецификаций. Частным, но важным, случаем этой проблемы является проблема асинхронного тестирования, когда имеется два компонента: реализация и известная среда передачи. Здесь также композиция конформной реализации со средой может быть не конформна композиции спецификации с этой средой. Для машин без приоритетов эта проблема решается с помощью монотонного преобразования спецификаций: композиция конформных реализаций оказывается конформной композиции преобразованных спецификаций. Для асинхронного тестирования: композиция конформной реализации со средой конформна композиции преобразованной спецификации с этой средой. Монотонное преобразование выполняется для /-семантик, в которых все отказы наблюдаемы, то есть =. В общем случае /-семантики сначала выполняется пополнение спецификации. Пополненная спецификация 50
эквивалентна (имеет тот же класс безопасных и тот же класс конформных реализаций) исходной спецификации в /-семантике, а кроме того, эквивалентна сама себе в /-семантике. Пополнение решает также проблему рефлексивности («самоприменимости») спецификации, которая в /-семантике может быть не конформна сама себе. Тем самым, совокупность преобразования пополнения и монотонного преобразования решает общую проблему монотонности и рефлексивности для любой /семантики [13,15,16]. Для машин с приоритетами проблемы монотонности и рефлексивности ещё не решены. Литература [1] И. Б. Бурдонов, А. С. Косачев, В. В. Кулямин. Использование конечных автоматов для тестирования программ. Программирование, 26(2):61–73, 2000. [2] И. Б. Бурдонов, А. С. Косачев, В. В. Кулямин. Асинхронные автоматы: классификация и тестирование. Труды ИСП РАН, 4:7–84, 2003. [3] И. Б. Бурдонов, А. С. Косачев, В. В. Кулямин. Неизбыточные алгоритмы обхода ориентированных графов. Детерминированный случай. Программирование, 29(5):59–69, 2003. [4] В. В. Кулямин, А. К. Петренко, А. С. Косачев, И. Б. Бурдонов. Подход UniTesK к разработке тестов. Программирование, 29(6):25–43, 2003. [5] V. V. Kuliamin, A. K. Petrenko, N. V. Pakoulin, A. S. Kossatchev, I. B. Bourdonov Integration of Functional and Timed Testing of Real-Time and Concurrent Systems. Proceedings of PSI 2003, LNCS 2890:450–461, Springer-Verlag, 2003. [6] И. Б. Бурдонов, А. С. Косачев, В. В. Кулямин. Неизбыточные алгоритмы обхода ориентированных графов. Недетерминированный случай. Программирование, 30(1):2–17, 2004. [7] И. Б. Бурдонов. Обход неизвестного ориентированного графа конечным роботом. «Программирование», 2004, No. 4. [8] И. Б. Бурдонов. Проблема отката по дереву при обходе неизвестного ориентированного графа конечным роботом. «Программирование», 2004, No. 6. [9] И. Б. Бурдонов. Исследование одно/двунаправленных распределённых сетей конечным роботом. Труды Всероссийской научной конференции "Научный сервис в сети ИНТЕРНЕТ". 2004. [10] И. Б. Бурдонов, А. С. Косачев. Тестирование компонентов распределенной системы. Труды Всероссийской научной конференции «Научный сервис в сети ИНТЕРНЕТ», Изд-во МГУ, 2005. [11] И. Б. Бурдонов, А. С. Косачев. Верификация композиции распределенной системы. Труды Всероссийской научной конференции «Научный сервис в сети ИНТЕРНЕТ», Изд-во МГУ, 2005. [12] I. Bourdonov, A. Kossatchev, V. Kuliamin. Formal Conformance Testing of Systems with Refused Inputs and Forbidden Actions. Proceedings of MBT 2006, Vienna, Austria, March 2006. [13] И. Б. Бурдонов, А. С. Косачев, В. В. Кулямин. Формализация тестового эксперимента. Программирование, 33(5):3–32, 2007.
51
[14] И. Б. Бурдонов, А. С. Косачев, В. В. Кулямин. Безопасность, верификация и теория конформности. Материалы Второй международной научной конференции по проблемам безопасности и противодействия терроризму, Москва, МНЦМО, 2007. [15] И. Б. Бурдонов, А. С. Косачев, В. В. Кулямин. Теория соответствия для систем с блокировками и разрушением. «Наука», 2008. [16] И. Б. Бурдонов. Теория конформности для функционального тестирования программных систем на основе формальных моделей. Диссертация на соискание учёной степени д.ф.-м.н., Москва, 2007. http://www.ispras.ru/~RedVerst/RedVerst/Publications/TR-01-2007.pdf. [17] М. П. Василевский. О распознавании неисправностей автомата. Кибернетика, 9(4):93–108, 1973. [18] V. Aho, A. T. Dahbura, D. Lee, M. Ü. Uyar. An Optimization Technique for Protocol Conformance Test Generation Based on UIO Sequences and Rural Chinese Postman Tours. IEEE Transactions on Communications, 39(11):1604–1615, 1991. [19] A. Blass, Y. Gurevich, L. Nachmanson, M. Veanes. Play to Test Microsoft Research. Technical Report MSR-TR-2005-04, January 2005. 5-th International Workshop on Formal Approaches to Testing of Software (FATES 2005). Edinburgh, July 2005. [20] J. Edmonds, E. L. Johnson. Matching. Euler Tours, and the Chinese Postman. Math. Programming 5:88–124, 1973. [21] S. Fujiwara, G. v. Bochmann. Testing Nondeterministic Finite State Machine with Fault Coverage. In J. Kroon, R. J. Heijink, and E. Brinksma, eds. IFIP Transactions, Proceedings of IFIP TC6 Fourth International Workshop on Protocol Test Systems, 1991, North-Holland, pp. 267–280, 1992. [22] R. J. van Glabbeek. The linear time-branching time spectrum. In J. C. M. Baeten and J. W. Klop, eds. CONCUR’90, LNCS 458:278–297, Springer-Verlag, 1990. [23] R. J. van Glabbeek. The linear time — branching time spectrum II; the semantics of sequential processes with silent moves. In E. Best, ed. Proceedings CONCUR’93, Hildesheim, Germany, August 1993, LNCS 715:66–81, Springer-Verlag, 1993. [24] J. B. Goodenough, S. L. Gerhart. Toward a theory of test data selection. IEEE Trans. Software Eng., 1(2):156–173, June 1975. [25] M. Grochtmann, K. Grimm. Classification trees for partition testing. Software Testing, Verification and Reliability, 3:63–82, 1993. [26] L. Heerink, J. Tretmans. Refusal Testing for Classes of Transition Systems with Inputs and Outputs. In T. Mizuno, N. Shiratori, T. Higashino, A. Togashi, eds. Formal Description Techniques and Protocol Specification, Testing and Verification. Chapman & Hill, 1997. [27] L. Heerink. Ins and Outs in Refusal Testing. PhD thesis, University of Twente, Enschede, The Netherlands, 1998. [28] D. Lee, M. Yannakakis. Testing Finite State Machines: State Identification and Verification. IEEE Trans. on Computers, 43(3):306–320, March 1994. [29] D. Lee, M. Yannakakis. Principles and Methods of Testing Finite State Machines — A Survey. Proceedings of the IEEE 84(8):1090–1123, 1996. [30] B. Legeard, F. Peureux, M. Utting. Automated boundary testing from Z and B. In Proc. of the Int. Conf. on Formal Methods Europe, FME'02, Copenhaguen, Denmark, July 2002, LNCS 2391:21–40, Springer, 2002. [31] G. Lestiennes , M.-C. Gaudel. Test de systemes reactifs non receptifs. Journal Europeen des Systemes Automatises, Modelisation des Systemes Reactifs, pp. 255–270. Hermes, 2005. [32] R. Milner. A Calculus of Communicating Processes. LNCS 92, Springer-Verlag, 1980.
52
[33] R. Milner. Modal characterization of observable machine behaviour. In G. Astesiano, C. Bohm, eds. Proceedings CAAP’81, LNCS 112:25–34, Springer, 1981. [34] R. Milner. Communication and Concurrency. Prentice-Hall, 1989. [35] A. Petrenko, N. Yevtushenko, G. v. Bochmann. Testing deterministic implementations from nondeterministic FSM specifications. Selected proceedings of the IFIP TC6 9-th international workshop on Testing of communicating systems, September 1996. [36] A. Petrenko, N. Yevtushenko, J. L. Huo. Testing Transition Systems with Input and Output Testers. Proc. IFIP TC6/WG6.1 15-th Int. Conf. Testing of Communicating Systems, TestCom’2003, pp. 129–145. Sophia Antipolis, France, May 2003. [37] H. Zhu, P. A. V. Hall, J. H. R. May. Software Unit Test Coverage and Adequacy. ACM Computing Surveys, 29(4):366–427, Dec. 1997.
53
LTS — это совокупность S=LTS(VS,L,ES,s0), где VS — непустое множество состояний, L — алфавит внешних действий, ESVS(L{,})VS — множество переходов, s0VS — начальное состояние. Множество F-трасс LTS S обозначается F(S). Множество состояний, достижимых из состояния s по трассе обозначается s after ; S after =def s0 after .
Эквивалентные семантики взаимодействия И. Б. Бурдонов, А. С. Косачев {igor, kos}@ispras.ru Аннотация. В статье рассматривается эквивалентность формальных семантик взаимодействия с точки зрения определяемых ими возможностей для тестирования. Дается строгое определение этого понятия, исследуются свойства соответствующего квазипорядка — «не меньшей мощности» одной семантики по отношению к другой. Приводятся необходимые и достаточные условия эквивалентности семантик.
1. Введение Тестирование существенно зависит от имеющегося набора тестовых возможностей по управлению и наблюдению за поведением тестируемой системы. Эти тестовые возможности формализуются в виде семантики взаимодействия. Поэтому представляет интерес сравнение различных семантик по мощности их возможностей тестирования. В данной статье исследуется вопрос о том, когда одна семантика «не сильнее», чем другая семантика, — то есть, когда все, что можно проверить тестированием с использованием первой семантики, можно и при помощи второй, — и когда семантики эквивалентны по мощности тестирования. Мы используем определения семантики взаимодействия, безопасности и конформности, которые даны в первом разделе нашей статьи «Системы с приоритетами: конформность, тестирование, композиция» [1]. Эти определения можно также найти в [2,4]. Повторим здесь кратко основные из них. Говорят, что задана /-семантика, если задан алфавит внешних действий L и два семейства его подмножеств: семейство кнопок (L), которым соответствуют наблюдаемые отказы, и семейство кнопок (L), которым соответствуют ненаблюдаемые отказы. Предполагается, что = и =L. Внутреннее действие обозначается символом , дивергенция (бесконечная последовательность -действий) — символом , разрушение — символом . -трасса — последовательность внешних действий и -отказов, быть может, завершающаяся дивергенцией или разрушением. F-трасса — трасса для =(L). Примеры таких семантик можно найти в [4-8]. 55
Отношение безопасности кнопки после -трассы в реализации: P safe in I after =def F(I) & uP u,F(I) & (P PF(I)). Безопасность кнопки после -трассы в спецификации — отношение safe by, удовлетворяющее следующим трём требованиям: 1) R safe by S after F(S) & uR u,F(S), 2) zF(S) & F(S) & T zT & uT u,F(S) P zP & P safe by S after , 3) Q safe by S after F(S) & uQ u,F(S) & vQ vF(S). -отказ R безопасен после трассы, если после трассы безопасна кнопка R. Действие z безопасно после трассы, если оно разрешается некоторой кнопкой zP, безопасной после трассы. -трасса безопасна, если в модели нет трассы , и каждый символ трассы безопасен после непосредственно предшествующего ему префикса трассы. Множества безопасных трасс реализации I и спецификации S обозначаются SafeIn(I) и SafeBy(S). Гипотеза о безопасности (безопасно-тестируемости) реализации для заданной спецификации: I safe for S =def (F(S) F(I))& SafeBy(S)SafeIn(I) P (P safe by S after P safe in I after ). Множество безопасных реализаций safe(S)={I|I safe for S}. Отношение безопасной конформности: I saco S =def I safe for S & SafeBy(S)SafeIn(I) P safe by S after obs(,P,I)obs(,P,S), где obs(,P,T) =def {u|uT & (uP u=P & P)}. Множество конформных реализаций (S)={I|I saco S}.
2. Квазипорядок и эквивалентность семантик Пусть в алфавите внешних действий L заданы две семантики 1/1 и 2/2: (11)=(22)=L. Будем говорить, что 1/1-семантика не сильнее 2/2-семантики и обозначать 1/12/2, если для каждой 56
спецификации S с отношением safe by1 в 1/1-семантике существует отношение safe by2 в 2/2-семантике, сохраняющее классы безопасных и конформных реализаций: safe1(S)=safe2(S) & 1(S)=2(S). Определим также: 1/12/2 =def 2/21/1. Отношение «не сильнее» для семантик является Теорема 1: квазипорядком: рефлексивно и транзитивно. Доказательство: Рефлексивность: S safe by1 Транзитивность: если S safe by1 safe by2 и S safe by2 safe by3 то S safe by1 safe by3 Лемма доказана.
safe1(S)=safe1(S) & 1(S)=1(S). safe1(S)=safe2(S) & 1(S)=2(S) safe2(S)=safe3(S) & 2(S)=3(S)), safe1(S)=safe3(S) & 1(S)=3(S).
Будем говорить, что 1/1- и 2/2-семантики эквивалентны и обозначать 1/12/2, если обе они не сильнее друг друга: 1/12/2 и 2/21/1. Очевидно, это отношение является эквивалентностью: рефлексивно, симметрично и транзитивно.
3. Необходимые условия квазипорядка Сначала исследуем вопрос о необходимых условиях отношения «не сильнее». Эти условия будут сформулированы и доказаны в следующих четырёх леммах, использующих примеры на Рис. 1.
Теорема 2: Если 1/12/2, то каждая 1-кнопка является также 2-кнопкой: 12. Доказательство: Допустим утверждение не верно: существует кнопка P1\2. Рассмотрим LTS-спецификацию S1, в которой в начальном состоянии определены разрушающие переходы по каждому действию tL\P, и один переход по некоторому действию zP. Выберем отношение safe by1, объявляющее кнопку P безопасной (после пустой трассы), а все остальные 1-кнопки опасными. Возможны два случая в зависимости от того, разрешается ли действие z какой-нибудь 2-кнопкой, безопасной по отношению safe by2. 1. По отношению safe by2 действие z разрешается некоторой безопасной кнопкой P`2. По допущению, P`P. Поскольку каждое действие tL\P разрушающее, а кнопка P` безопасна в спецификации, должно быть tP`. Следовательно, P`P. Тогда найдётся действие z`P\P`. Для реализации I1, в которой есть ненаблюдаемый в 2/2-семантике отказ P`, имеем I1 safe for2 S1. Поскольку по отношению safe by1 единственная 1-кнопка, безопасная после пустой трассы, это кнопка P, имеем I1 safe for1 S1. Но это противоречит 1/12/2. 2.
По отношению safe by2 действие z не разрешается безопасными 2кнопками. Тогда по отношению safe by2 после пустой трассы вообще нет безопасных 2-кнопок: 2-кнопка, вложенная в P, не содержит действие z и, следовательно, не разрешает ни одного действия в спецификации, а такая 2-кнопка опасна; 2-кнопка, не вложенная в P, разрушающая по некоторому действию tL\P. А тогда I2 safe for2 S1. В то же время, поскольку по отношению safe by1 1-кнопка P безопасна после пустой трассы, имеем I2 safe for1 S1. Но это противоречит 1/12/2.
В обоих случаях мы пришли к противоречию, следовательно, наше допущение не верно, а утверждение Леммы верно. Если 1/12/2, то каждая 2-кнопка представима в Теорема 3: виде объединения 1- и 1-кнопок: P2 11 P=. Доказательство: Допустим утверждение не верно: существует кнопка P2, которую нельзя представить в виде объединения 1- и 1-кнопок. Тогда найдётся такое действие zP, которое не разрешается ни одной 1- или 1кнопкой, вложенной в P. Рассмотрим LTS-спецификацию S1, в которой в начальном состоянии определены разрушающие переходы по каждому действию tL\Q, и один переход по действию z. Для любого отношения
Рис. 1. Примеры для доказательств Лемм 2-5. 57
58
safe by1 нет безопасной 1- или 1-кнопки, разрешающей действие z. В 2/2-семантике есть неразрушающая кнопка P, разрешающая действие z. Поэтому для любого отношения safe by2 должна найтись безопасная кнопка, разрешающая действие z. Но в таком случае реализация I3 safe for1 S1, но I3 safe for2 S1, что противоречит 1/12/2. Мы пришли к противоречию, следовательно, наше допущение не верно, а утверждение Леммы верно. Если 1/12/2, то каждая 1-кнопка представима в Теорема 4: виде объединения конечного числа 2-кнопок: P1 2 P= & -конечно. Доказательство: Допустим утверждение не верно: некоторая кнопка P1 не представима в виде объединения конечного числа 2-кнопок. Рассмотрим LTS-спецификацию S2, в которой в начальном состоянии определены разрушающие переходы по каждому действию tL\P, а для каждого действия zP определён -переход в состояние, из которого выходит только переход по действию z. По отношению safe by1 после пустой трассы кнопка P безопасна, но отказ P отсутствует. В реализации I2 в этой ситуации имеется отказ P. Следовательно, I2 saco1 S2. По отношению safe by2 после пустой трассы безопасна такая и только такая 2-кнопка, которая вложена в P. Более того, после любой конечной трассы таких 2-отказов безопасна тоже такая и только такая 2-кнопка, которая вложена в P. Любая конечная последовательность 2-отказов, вложенных в P, конформна: она реализуется в состоянии после -перехода, соответствующего действию z, которое не принадлежит объединению этих 2-кнопок, а такое действие всегда найдётся, поскольку P не представимо в виде объединения конечного числа 2-кнопок. Тем самым, I2 saco2 S2. Но это противоречит 1/12/2. Мы пришли к противоречию, следовательно, наше допущение не верно, а утверждение Леммы верно. Если 1/12/2, то каждая 2-кнопка представима в Теорема 5: виде объединения конечного числа 1-кнопок: R2 1 R= & -конечно.
этих -трасс эта кнопка одинаково опасна или безопасна по отношению safe in. Это происходит тогда, когда в любой реализации эти две -трассы заканчиваются в одном и том же множестве состояний. А это, в свою очередь, происходит тогда, когда -трассы эквивалентны: в этих -трассах на соответствующих местах стоят одинаковые внешние действия или последовательности -отказов с одинаковым множеством отвергаемых внешних действий. Этим последовательностям -отказов соответствуют стабильные состояния, в которых нет переходов по всем действиям, принадлежащим каким-нибудь отказам в последовательности. Эквивалентность -трасс формально определяется так: 12 =def 1=11z11…1nz1n1n+1 & 2=21z21…2nz1n2n+1 & i=1..n z1i=z2i & j=1..n+1 Im(1j)=Im(2j), где zki — внешнее действие, а kj — трасса отказов. Теорема 6: Необходимым и достаточным условием того, что две трассы в любой модели заканчиваются в одном и том же множестве состояний, является их эквивалентность. Доказательство: 1. Сначала докажем достаточность. Доказательство будем вести по индукции. В любой модели пустые трассы эквивалентны и заканчиваются в одном множестве состояний, поскольку равны. Пусть в некоторой модели S с начальным состоянием s0 трассы 1 и 2 эквивалентны и заканчиваются в одном и том же множестве состояний: s0 after 1 = s0 after 2. Рассмотрим их продолжение одним и тем же внешним действием z: s0 after 1z = {s after z|s(S after 1)} = {s after z|s(S after 2)} = S after 2z. Теперь рассмотрим продолжение трасс последовательностями отказов 1 и 2 с одним и тем же множеством отвергаемых внешних действий Im(1)=Im(2):
Доказательство аналогично доказательству Леммы 4.
s0 after 11 = {s(S after 1)|z{,}Im(1) sz} = {s(S after 2)|z{,}Im(1) sz} = s0 after 22.
4. Нормализация отношения safe by Требования к отношению safe by однозначно определяют безопасность кнопок, но оставляют достаточно много свободы в объявлении безопасных и опасных -кнопок. В некоторых случаях можно говорить о «несогласованности» отношения safe by в следующем смысле: хотя некоторая -кнопка объявлена безопасной после одной -трассы спецификации и опасной — после другой -трассы, в любой реализации после 59
Достаточность доказана. 2. Теперь докажем необходимость. Вместо исходной LTS-спецификации S, в которой есть трассы 1 и 2, возьмём соответствующую ей «расплетённую» LTS T(S). Её состояниями будут -трассы исходной LTS, начальное состояние — 60
пустая трасса. Переход u` по внешнему действию или разрушению u проводится тогда и только тогда, когда `=u. Переход ` проводится тогда и только тогда, когда трасса не заканчивается отказом, а `=, где непустая трасса отказов. Нетрудно показать, что множества -трасс LTS S и T(S) совпадают. Допустим трассы 1 и 2 не эквивалентны. Рассмотрим первое место в этих трассах, где нарушается их эквивалентность. Здесь возможны два случая. 2.1.
Несовпадение действий: z1z2,.
1=1z11,
2=2z22,
12,
Поскольку 12, имеем P(S) after 1 = P(S) after 2. Граф LTS T(S) является деревом: в начальном состоянии не заканчивается ни один переход, а в каждое другое состояние ведёт только один переход. Поэтому в этой LTS трассы 1z1 и 2z2 заканчиваются в двух множествах состояний таких, что ни одно состояние одного множества не достижимо из состояния другого множества. Отсюда следует, что трассы 1 и 2 заканчивается в разных (даже не пересекающихся) множествах состояний LTS P(S). 2.2.
Несовпадение множеств отвергаемых внешних действий: 1=111, 2=222, 12, Im(1)Im(2), 1 и 2 не заканчиваются отказами, а 1 и 2 не начинаются с отказов. Поскольку 12, имеем P(S) after 1 = P(S) after 2. Эти множества содержат состояния 1 и 2, из которых ведут и 222. Добавим новое переходы 111 терминальное состояние, в которое для i=1,2 проведём из состояния ii все переходы по действиям z{zi}Im(i), если трасса i начинается с внешнего действия zi, или zIm(i), если трасса i пуста. После такого добавления, очевидно, трассы 1 и 2 сохраняются в LTS. Но теперь либо трасса 11 не заканчивается в состоянии 22, либо трасса 22 не заканчивается в состоянии 11 (либо и то и другое). Какими бы ни были продолжения 1 и 2, теперь трассы 1 и 2 заканчиваются в разных множествах состояний.
Необходимость доказана. Лемма доказана. 61
Отношение safe by будем называть нормальным, если оно определяет одинаковые безопасные кнопки после эквивалентных -трасс. Любое отношение safe by можно нормализовать, если после каждой -трассы объявить безопасными те и только те кнопки, которые исходным отношением объявлены безопасными после какой-нибудь -трассы, эквивалентной -трассе . Нормализованное отношение safe by удовлетворяет всем Теорема 7: трём требованиям, предъявляемым к такому отношению. При нормализации сохраняются класс безопасных и класс конформных реализаций. Доказательство: Оба утверждения Леммы непосредственно следуют из Леммы 6, поскольку эквивалентные трассы заканчиваются в одном и том же множестве состояний как в спецификации, так и в любой реализации, в которой такие трассы есть.
5. Достаточные условия квазипорядка Мы покажем, что совокупность условий Лемм 2-5 является не только необходимым, но и достаточным условием 1/12/2. Будем считать, что эти условия выполнены. Поскольку нормализация сохраняет безопасные и конформные реализации (Лемма 7), а отношение «не сильнее» является квазипорядком (Лемма 1), нам достаточно рассматривать только нормализованные отношения safe by. Сначала введём отображение 1-трасс в 2-трассы и обратно. Мы будем обозначать: 1+ = 2 и 2+ = 1. Для i=1,2 отображение fi каждую i-трассу превращает в i+-трассу, заменяя каждый i-отказ Ri на конечную последовательность i+-отказов Ri1,Ri2,…,Rin, объединение которых совпадает с ним: Ri=Ri1Ri2…Rin. Заметим, что отображение fi переводит трассу в эквивалентную ей. Поэтому множество состояний после любой i-трассы совпадает с множеством состояний после i+-трассы fi(). Также отметим, что разрушаемость или неразрушаемость кнопки после любой трассы модели не зависит от семантики и определяется только самой моделью. Определим отношение safe by2 в 2/2-семантике. Безопасность 2-кнопок определяется однозначно требованием 1): 2-кнопка безопасна тогда и только тогда, когда она неразрушающая после 2-трассы, а трасса не продолжается дивергенцией. 2-кнопку, являющуюся также 1-кнопкой, объявим безопасной после 2-трассы тогда и только тогда, когда она безопасна safe by2, это после 1-трассы f2(). В силу нормализации отношения означает, что кнопка безопасна после каждой 1-трассы, эквивалентной трассе f2(). Остальные 2-кнопки объявим опасными после любых 2-трасс. 62
Определённое таким образом отношение safe by2 Теорема 8: удовлетворяет всем трём требованиям, предъявляемым к такого рода отношению. Если отношение safe by1 нормально, то отношение safe by2 также нормально. Доказательство: 1.
Требование 1) выполнено по определению.
2.
Докажем выполнение требования 2). Пусть 2-трасса продолжается в спецификации действием z, которое разрешается некоторой неразрушающей кнопкой P2. Если это 2-кнопка, то она же и безопасна. Если это 2-кнопка, то она совпадает с объединением 1- и 1-кнопок P2=P21P22…P2n, каждая из которых неразрушающая после 2-трассы . Для некоторого i имеет место zP2i и кнопка P2i неразрушающая после 2-трассы . А тогда кнопка P2i неразрушающая после 1-трассы f2(). В таком случае в 1/1-семантике существует кнопка P1, которая разрешает действие z и безопасна после 1-трассы f2(). Если это 1кнопка, то она же является 2-кнопкой и безопасна после 2-трассы . Если это 1-кнопка, то она представима в виде объединения 2-кнопок P1=P11P12…P1n, каждая из которых неразрушающая после 1-трассы f2(). Тогда найдётся такое j, что zP1j и 2-кнопка P1j неразрушающая после 2-трассы . А тогда 2-кнопка P1j безопасна после 2-трассы . Выполнение требования 2) доказано.
3.
Выполнение требования 3) следует из того, что из 2-кнопок безопасной после 2-трассы может быть только 1-кнопка Q1. А тогда она безопасна после 1-трассы f2(). Следовательно, она разрешает некоторое действие, которым продолжается 1-трасса f2(). А тогда этим же действием продолжается 2-трассы , поскольку она заканчивается в том же множестве состояний.
4.
Покажем, что отношение отношение safe by1.
safe by2
нормально, если нормально
Пусть есть две эквивалентные 2-трассы 12. Поскольку они заканчиваются в одном и том же множестве состояний спецификации, неразрушающие 2-кнопки после этих трасс одинаковые, следовательно, одинаковые безопасные 2-кнопки. Также имеем f2(1)f2(2). Отсюда следует, что после 1-трасс f2(1) и f2(2) одинаковые безопасные 1-кнопки. А тогда после 2-трасс 1 и 2 одинаковые безопасные 2кнопки (совпадающие с этими безопасными 1-кнопками).
Теперь исследуем вопрос о соотношении безопасных 1- и 2-трасс, связанных отображениями f2 и f1, и о безопасности кнопок после таких трасс. Теорема 9: Если i-трасса безопасна по safe byi, то i+-трасса fi() безопасная по safe byi+. Пусть i-кнопка Ri безопасна по safe byi после i-трассы и совпадает с объединением i+-кнопок Ri=Ri1Ri2…Rin. Тогда после i+-трассы fi() все эти i+-кнопки безопасны по safe byi+. Пусть i-кнопка Qi безопасна после i-трассы по safe byi. Тогда она является также i+-кнопкой и безопасна после fi+() по safe byi+. Доказательство: Пусть после i-трассы i-кнопка Ri безопасна по safe byi и совпадает с объединением i+-кнопок Ri=Ri1Ri2…Rin. Трассы и fi() заканчиваются в одном и том же множестве состояний, а кнопки с наблюдаемым отказом безопасны тогда и только тогда, когда они неразрушаемые. Поэтому после i+-трассы fi() все эти i+-кнопки безопасны по safe byi+. Из этого утверждения следует, что, если i-трасса безопасна по safe byi, то i+-трасса fi() безопасная по safe byi+. Пусть i-кнопка безопасна после i-трассы по safe byi. Если i=2, то 2кнопка объявляется безопасной после по safe by2, если она является 1кнопкой, безопасной по safe by1 после f2(), что и требовалось показать. Если i=1, то 1-кнопка является также 2-кнопкой, которая объявляется безопасной по safe by2 после f1(), если, как 1-кнопка, она безопасна по safe by1 после f2(f1()). Трассы и f2(f1()) эквивалентны, следовательно, для нормализованного отношения safe by1 безопасность кнопок после них одинаковая: если кнопка безопасна после , то она также безопасна после f2(f1(). А тогда эта кнопка, как 2-кнопка, безопасна по safe by2 после f1(), что и требовалось показать. Совокупность условий Лемм 1 и 2 является не только Теорема 1: необходимым, но и достаточным условием R1/Q1R2/Q2. Доказательство: Необходимость условий доказана в Лемме 1 и Лемме 2. Докажем достаточность совокупности этих условий. 1.
Лемма доказана. 63
64
Докажем, что, если реализация безопасна для спецификации с отношением safe by2, то она безопасна этой же спецификации с отношением safe by1. Допустим, это не так. Тогда найдётся такая реализация безопасная для спецификации с отношением safe by2, такая безопасная в спецификации по safe by1 1-трасса , которая имеется также в реализации, и найдётся такая 1- или 1-кнопка P1, которая безопасна после в спецификации по safe by1, но опасна в реализации после этой трассы. По доказанному, 2-трасса f1() безопасная по safe by2.
1.1.
Пусть P1 1-кнопка. Тогда она совпадает с объединением 2кнопок P1=P11P12…P1n, и все эти 2-кнопки безопасны по safe by2 после 2-трассы f1(). Но тогда все они безопасны после 2-трассы f1() в реализации. А отсюда следует, что 1-кнопка P1 безопасна после 1-трассы в реализации, что не верно по допущению.
1.2.
Пусть P1 1-кнопка. Тогда она также 2-кнопка и безопасна по safe by2 после 2-трассы f1(). А тогда она безопасна после 2трассы f1() в реализации. Следовательно, она безопасна в реализации после 1-трассы , что противоречит допущению.
Итак, мы пришли к противоречию, следовательно, наше допущение не верно, и утверждение доказано: если реализация безопасна для спецификации с отношением safe by2, то она безопасна этой же спецификации с отношением safe by1. 2.
Докажем, что, если реализация конформна для спецификации с отношением safe by2, то она конформна этой же спецификации с отношением safe by1. Допустим, это не так. Тогда найдётся такая реализация конформная для спецификации с отношением safe by2, такая безопасная в спецификации по safe by1 1-трасса , которая имеется также в реализации, и найдётся такая 1- или 1-кнопка P1, которая безопасна после в спецификации по safe by1, и, по доказанному, безопасная в реализации после этой трассы, что в реализации трасса продолжается символом u, которое либо a) является действием u=zP1, либо b) отказом u=P1, если P11, а в спецификации трасса не продолжается символом u. По доказанному, 2-трасса f1() безопасная по safe by2. Очевидно также, что 2-трасса f1() в реализации продолжается символом u, а в спецификации — нет. 2.1.
Пусть P1 1-кнопка. Тогда она является также 2-кнопкой и безопасна по safe by2 после 1-трассы f1(). Тем самым 2-трасса f1() безопасна в спецификации по safe by2, после неё безопасна по safe by2 2-кнопка P1, u=zP1, в реализации трасса продолжается действием u=z, а в спецификации — нет. Но это противоречит конформности реализации для спецификации с отношением safe by2.
2.2.
Пусть P1 1-кнопка. Тогда она совпадает с объединением 2кнопок P1=P11P12…P1n, и все эти 2-кнопки безопасны по safe by2 после 2-трассы f1(). 65
2.2.1.
Пусть u=zP1. Тогда для некоторого i имеем u=zP1i. Тем самым 2-трасса f1() безопасна в спецификации по safe by2, после неё безопасна по safe by2 2-кнопка P1i, u=zP1i, в реализации трасса продолжается действием u=z, а в спецификации — нет. Но это противоречит конформности реализации для спецификации с отношением safe by2.
2.2.2.
Пусть u=P1. Тогда 2-трасса f1() безопасна в спецификации по safe by2, после неё безопасны по safe by2 все 2-кнопки P1i, и в реализации трасса продолжается трассой отказов P11,P12,…,P1n. Но тогда в спецификации 2-трасса f1() также должна продолжаться этой трассой отказов. А в таком случае 2трасса f1() продолжается отказом P1. Следовательно, 1-трасса также продолжается отказом P1, что не верно.
Мы пришли к противоречию, и, значит, наше допущение не верно, а доказываемое утверждение верно: если реализация конформна для спецификации с отношением safe by2, то она конформна этой же спецификации с отношением safe by1. 3.
66
Докажем, что, если реализация безопасна для спецификации с отношением safe by1, то она безопасна этой же спецификации с отношением safe by2. Допустим, это не так. Тогда найдётся такая реализация безопасная для спецификации с отношением safe by1, такая безопасная в спецификации по safe by2 2-трасса , которая имеется также в реализации, и найдётся такая 2- или 2-кнопка P2, которая безопасна после в спецификации по safe by2, но опасна в реализации после этой трассы. По доказанному, 1-трасса f2() безопасная по safe by1. 3.1.
Пусть P2 2-кнопка. Тогда, по доказанному, она совпадает с объединением 1-кнопок P2=P21P22…P2n, и все эти 1-кнопки безопасны по safe by1 после 1-трассы f2(). Но тогда все они безопасны после 1-трассы f2() в реализации. А отсюда следует, что 2-кнопка P2 безопасна после 2-трассы в реализации, что не верно по допущению.
3.2.
Пусть P2 2-кнопка. Тогда она должна быть также 1-кнопкой, безопасной по отношению safe by1 после 1-трассы f2(). Но тогда в реализации эта кнопка безопасна после 1-трассы f2(),
следовательно, безопасна после 2-трассы , что противоречит допущению.
все 1-кнопки P2i, и в реализации трасса продолжается трассой отказов P21,P22,…,P2n. Но тогда в f2() также должна спецификации 1-трасса продолжаться этой трассой отказов. А в таком случае 1трасса f2() продолжается отказом P2. Следовательно, 2-трасса также продолжается отказом P2, что не верно.
Итак, мы пришли к противоречию, следовательно, наше допущение не верно, и утверждение доказано: если реализация безопасна для спецификации с отношением safe by1, то она безопасна этой же спецификации с отношением safe by2. 4.
Докажем, что, если реализация конформна для спецификации с отношением safe by1, то она конформна этой же спецификации с отношением safe by2. Допустим, это не так. Тогда найдётся такая реализация конформная для спецификации с отношением safe by1, такая безопасная в спецификации по safe by2 2-трасса , которая имеется также в реализации, и найдётся такая 2- или 2-кнопка P2, которая безопасна после в спецификации по safe by2, и, по доказанному, безопасная в реализации после этой трассы, что в реализации трасса продолжается символом u, которое либо a) является действием u=zP2, либо b) отказом u=P2, если P22, а в спецификации трасса не продолжается символом u. По доказанному, 1-трасса f2() безопасная по safe by1. Очевидно также, что 1-трасса f2() в реализации продолжается символом u, а в спецификации — нет. 4.1.
Пусть P2 2-кнопка. Тогда, поскольку она безопасна, она является также 1-кнопкой и безопасна по safe by1 после 1-трассы f2(). Тем самым 1-трасса f2() безопасна в спецификации по safe by1, после неё безопасна по safe by1 1-кнопка P2, u=zP2, в реализации трасса продолжается действием u=z, а в спецификации — нет. Но это противоречит конформности реализации для спецификации с отношением safe by1.
4.2.
Пусть P2 2-кнопка. Тогда она совпадает с объединением 1кнопок P2=P21P22…P2n, и все эти 1-кнопки безопасны по safe by1 после 1-трассы f2(). 4.2.1.
Пусть u=zP2. Тогда для некоторого i имеем u=zP2i. Тем самым 1-трасса f2() безопасна в спецификации по safe by1, после неё безопасна по safe by1 1-кнопка P2i, u=zP2i, в реализации трасса продолжается действием u=z, а в спецификации — нет. Но это противоречит конформности реализации для спецификации с отношением safe by1.
4.2.2.
Пусть u=P2. Тогда 1-трасса f2() безопасна в спецификации по safe by1, после неё безопасны по safe by1 67
Мы пришли к противоречию, и, значит, наше допущение не верно, а доказываемое утверждение верно: если реализация конформна для спецификации с отношением safe by1, то она конформна этой же спецификации с отношением safe by2. Теорема доказана.
6. Эквивалентные и минимальные семантики Из Теоремы 1 непосредственно следует следующая теорема. Необходимым и достаточным условием эквивалентности Теорема 2: двух семантик является совпадение семейств Q-кнопок и представимость каждой R-кнопки одной семантики в виде объединения конечного числа R-кнопок другой семантики. Представляет интерес определение минимальной (по вложенности семейств кнопок) эквивалентной семантики. Иными словами, какие кнопки можно удалить из заданной /-семантики так, чтобы получилась семантика, эквивалентная исходной. По Теореме 2, -кнопки удалять нельзя, а -кнопку можно удалить в том случае, когда она равна объединению конечного числа остающихся -кнопок. -кнопку будем называть разложимой, если её можно представить в виде объединения конечного числа -кнопок, отличных от неё самой. Теорема 3: В минимальной эквивалентной R0/Q-семантике, если она существует, семейство R0 совпадает с множеством неразложимых Rкнопок из R. Доказательство: Если кнопка P, то, в силу эквивалентности семантик, она может быть представлена в виде объединения конечного числа 0-кнопок. Но тогда, если кнопка P неразложима, она просто должна быть 0-кнопкой. Наоборот, если бы кнопка P0 была разложима, то её можно было бы представить в виде объединения конечного числа -кнопок, отличных от неё самой P=P1P2…Pn. В силу эквивалентности семантик, каждая кнопка Pi, в свою очередь, может быть представлена в виде объединения конечного числа 0-кнопок Pi=Pi1Pi2…Pini, которые очевидно, тоже отличны от P. А тогда кнопку P можно представить в виде объединения конечного числа 68
0-кнопок Pij, отличных от неё самой, что противоречит минимальности 0/-семантики. Однако минимальная эквивалентная семантика не обязательно существует. Примером может служить семантика, в которой -кнопки — это все бесконечные подмножества бесконечного алфавита: все -кнопки разложимы. Теорема 4: Для существования минимальной эквивалентной R0/Qсемантики необходимо и достаточно, чтобы любая разложимая R-кнопка разлагалась в объединение конечного числа неразложимых R-кнопок. Доказательство: Необходимость условия следует из эквивалентности (любая -кнопка представима в виде объединения конечного числа 0-кнопок) и Теоремы 3: в минимальной эквивалентной 0/-семантике семейство 0 совпадает с множеством неразложимых -кнопок из . Покажем достаточность: если условие выполнено, то 0/-семантика эквивалентна и минимальна, где 0 — это множество неразложимых -кнопок из . Эта семантика эквивалентна по условию. Если бы семантика не была минимальной, то какую-то кнопку можно было бы удалить из неё. А это возможно лишь тогда, когда эта кнопка разлагается в объединение конечного числа 0-кнопкой, следовательно, является разложимой, что не верно. Если конечно число бесконечных R-кнопок (в частности, Теорема 5: конечно семейство R), то минимальная эквивалентная семантика существует. Доказательство: Процесс разложения любой конечной разложимой -кнопки заканчивается через конечное число шагов (на каждом шаге разлагаем все получившиеся разложимые кнопки) в силу конечности самой кнопки (на каждом шаге получаются конечные множества меньшей мощности). В силу этого, процесс разложения бесконечной -кнопки также заканчивается через конечное число шагов, поскольку конечно число бесконечных -кнопок (на каждом шаге получаем кнопки, каждая из которых либо строго вложенная бесконечная кнопка, а число бесконечных кнопок конечно, либо конечна). Если минимальная эквивалентная семантика существует, Теорема 6: то она же является наименьшей. Доказательство: Это непосредственно следует из Теоремы 3.
7. Пример: семантики отношений ioco и ioco Отношение конформности ioco (Input-Output Conformance) было предложено Яном Тритмансом [8-9] для реактивных систем. Взаимодействие с такими системами сводится к обмену сообщениями между реализацией и тестом. Алфавит L внешних действий разбивается на множество ?L стимулов — сообщений, передаваемых из теста в реализацию (обозначаются с префиксом “?”), и множество !L реакций — сообщений, передаваемых из реализации в тест (обозначаются с префиксом “!”). Единственный наблюдаемый отказ — 69
отсутствие реакций, называемый стационарностью (quiescence) и обозначаемый символом =!L. Генерация тестов для отношения ioco предполагает, что, кроме единственной -кнопки “”, для каждого стимула ?x существует -кнопка “{?x}”, с помощью которой в реализацию можно послать этот стимул. Отказ, возникающий при посылке одного стимула, называется блокировкой этого стимула и ненаблюдаем. Обозначим семейства этой семантики 0={}={!L} и 0={{?x}|?x?L}. По Теореме 2, не существует другой семантики, эквивалентной ей. В то же время Тритманс заявляет, что использование таких дополнительных тестовых возможностей, как посылка нескольких стимулов или совмещение посылки стимулов с приёмом всех реакций, не увеличивает мощность тестирования и приводит только к излишнему недетерминизму теста. Иными словами, семантика, получаемая добавлением 1-кнопок вида {{?x1},{?x2},…} или {}{{?x1},{?x2},…}, эквивалентна исходной. Причина расхождения в том, что Тритманс определяет отношение конформности не на всём классе реализаций, а на его подклассе. Требуется, чтобы реализация была, во-первых, строго-конвергентной, а, во-вторых, всюду-определённой по стимулам (input-enabled). Такая реализация в каждом своём достижимом состоянии не имеет дивергенции, а в каждом достижимом стабильном состоянии принимает все стимулы. Кроме того, Тритманс рассматривает модели без разрушения. На таком подклассе реализаций, действительно, добавление в исходную семантику указанных 1-кнопок даёт эквивалентную семантику, поскольку в реализации не возникает ненаблюдаемых отказов, а весь алфавит внешних действий покрывается кнопками исходной семантики. Этот пример показывает, что эквивалентность семантик может существенно изменяться, если её рассматривать на подклассах реализаций. Разумеется, для того, чтобы опираться на такую модифицированную эквивалентность, нужны обоснованные гипотезы о возможных классах тестируемых реализаций. В [3] нами предложена семантика для реактивных систем с разрушением и дивергенцией, допускающая наблюдение блокировок стимулов: 2=00 и 2=, и соответствующее отношение конформности ioco. В отличие от ioco, для такой семантики, по Теореме 2, эквивалентные семантики существуют на классе всех реализаций. Эквивалентной будет любая семантика, получающаяся добавлением любых -кнопок, каждая из которых разрешает конечное число стимулов и, быть может, все реакции. Это почти те же самые кнопки, которые для отношения ioco можно добавлять на подклассе строго-конвергентных и всюду-определённых по стимулам реализаций. Отличие в том, что, во-первых, для ioco эти кнопки добавляются как -кнопки, а для ioco — как -кнопки, и, во-вторых, для ioco требуется конечность числа стимулов, которые могут посылаться нажатием одной кнопки. Последнее объясняется тем, что наблюдение отказа при 70
попытке послать бесконечное число стимулов, не эквивалентно наблюдению конечной последовательности блокировок стимулов. Литература [1] [2] [3] [4]
[5] [6] [7]
[8] [9]
И. Б. Бурдонов, А. С. Косачев. Системы с приоритетами: конформность, тестирование, композиция. Опубликовано в этом сборнике. И. Б. Бурдонов, А. С. Косачев, В. В. Кулямин. Формализация тестового эксперимента. Программирование, 33(5):3–32, 2007. И. Б. Бурдонов, А. С. Косачев, В. В. Кулямин. Теория соответствия для систем с блокировками и разрушением. «Наука», 2008. И. Б. Бурдонов. Теория конформности для функционального тестирования программных систем на основе формальных моделей. Диссертация на соискание учёной степени д.ф.-м.н., Москва, 2007. http://www.ispras.ru/~RedVerst/RedVerst/Publications/TR-01-2007.pdf. L. Heerink. Ins and Outs in Refusal Testing. PhD thesis, University of Twente, Enschede, The Netherlands, 1998. G. Lestiennes G., M.-C. Gaudel. Test de systemes reactifs non receptifs. Journal Europeen des Systemes Automatises, Modelisation des Systemes Reactifs, pp. 255–270. Hermes, 2005. Petrenko, N. Yevtushenko, J. L. Huo. Testing Transition Systems with Input and Output Testers. Proc. IFIP TC6/WG6.1 15-th Int. Conf. Testing of Communicating Systems, TestCom’2003, pp. 129–145. Sophia Antipolis, France, May 2003. J. Tretmans. Conformance testing with labelled transition systems: implementation relations and test generation. Computer Networks and ISDN Systems, 29(1):49–79, Dec. 1996. J. Tretmans. Test Generation with Inputs, Outputs and Repetitive Quiescence. In: Software-Concepts and Tools, 17(3), 1996.
71
Многопоточное тестирование программных интерфейсов В. С. Мутилин
[email protected] Аннотация. В статье описывается новый метод функционального тестирования параллельных программ, предоставляющих программный интерфейс, методы (процедуры) которого можно вызывать из нескольких потоков одновременно. Этот метод, названный Sapsan, позволяет проверять одно из распространенных требований к таким программам — требование сериализуемости интерфейса, заключающееся в том, что в любом состоянии программы результат параллельного выполнения методов интерфейса в нескольких потоках эквивалентен некоторому последовательному выполнению этих же методов. Это требование является формализацией широко используемого понятия thread-safety.
1. Введение В последнее время многопоточное программирование получило широкое распространение. Для повышения производительности программ их выполнение все чаще организуют в виде нескольких потоков, которые работают параллельно. Но разработка многопоточных программ намного сложнее последовательных. Это связано с тем, что порядок, в котором будут выполнены инструкции разных потоков, заранее непредсказуем и разработчик должен предусмотреть корректную работу программы при всех возможных чередованиях инструкций. В данной статье мы рассматриваем программы, предоставляющие программный интерфейс, через который с ними взаимодействуют другие программы. Интерфейс состоит из отдельных методов (процедур), описанных на языке программирования, которые можно вызывать с различными значениями параметров. Кроме того, методы интерфейса могут быть вызваны из различных потоков одновременно. Одно из распространенных требований к многопоточным программам — требование сериализуемости. Оно, по сути, является формализацией широко используемого понятия thread-safety. Пусть в некотором состоянии программы выполняется несколько потоков, вызывающих по одному интерфейсному методу. Будем называть выполнение потоков сериализуемым, если результат 73
выполнения эквивалентен некоторому последовательному выполнению этих же потоков в том же исходном состоянии. На практике требования сериализуемости широко распространены. Отметим два распространенных класса таких систем: интерфейсы промежуточного уровня клиент-серверных приложений и библиотеки, предназначенные для многопоточного использования. Обычно предполагается, что несколько клиентов могут одновременно работать в рамках клиент-серверного приложения, не замечая присутствия друг друга. Если два клиента совершают какие-то операции, то результат должен быть такой же, как если бы они были выполнены последовательно. Данный класс систем чрезвычайно широк, так как практически все современные системы, предоставляющие сервисы, могут обслуживать несколько клиентов одновременно. В библиотеках, предназначенных для многопоточного использования, такое требование выдвигается по умолчанию. Функциональные требования описываются для каждого метода в отдельности, но кроме того требуется, чтобы библиотечные методы было безопасно вызывать из нескольких потоков (thread-safe). В наших терминах это и есть требование сериализуемости. Одним из наиболее распространенных способов проверки свойств параллельных программ является метод проверки моделей (model checking), осуществляющий поиск чередований инструкций в параллельных потоках. В последнее время инструменты проверки моделей сделали существенный шаг вперед. Стало возможным проверять свойства для программ, написанных на широко распространенных языках программирования, а не на простых модельных языках. Так инструмент Java PathFinder [12], способен проверять свойства программ на языке программирования Java, а инструмент VeriSoft [8] предназначен для проверки программ на языке C. Однако попытки применения этих инструментов для решения задачи проверки сериализуемости программы, сталкиваются с рядом сложностей. Во-первых, чтобы запустить эти инструменты требуется подготовить окружение, т.е. задать набор потоков, вызывающих методы интерфейса с некоторыми значениями параметров, и начальное состояние. Но так как требование сериализуемости формулируется для неограниченного числа потоков, то проверив сериализуемость на определенных конечных их наборах, мы не можем быть уверенны, что оно выполнено для программы в целом. Вовторых, поиск всех возможных выполнений программы занимает большое время. В зависимости от количества потоков самым современным инструментам требуется для этого от нескольких минут до нескольких часов. Метод Sapsan для проверки сериализуемости, описанный в данной работе, в принципе может быть применен для широкого класса программ на разных языках программирования. Однако в статье детали метода рассматриваются только для программ, написанных на языке программирования Java.
74
2. Формальная постановка задачи
составляют все множество потоков и выполнение
Выполнение программы — это последовательность пар
(t i1 , b j1 ),..., (t in , b jn ),
где t ik — выполняемый поток, b jk — инструкция потока. Будем называть
ek (t ik , b jk ) единицей выполнения. Единица выполнения переводит систему из одного состояния в другое, что мы i i обозначаем как s s ' . Тот факт, что выполнение e1 ,..., en переводит
( t ,b )
систему из состояния s в состояние s ' , обозначаем
s s'. e1 ,...,en
Если в программе возможна последовательность инструкций представленная в выполнении, то такое выполнение называем достижимым в этой программе. Иначе, такое выполнение будем называть недостижимым в программе.
b1 ,..., bn , выполняемых в одном потоке. Из любого выполнения e1 ,..., en можно выделить путь для потока t ' , выбрав из единиц выполнения ek (t k , bk ), в Путь
в
потоке
—
это
последовательность
инструкций
которых t k t ' , инструкции bk в том же порядке. И обратно, если у нас есть пути p1 ,..., p k в потоках t1 ,..., t k , то мы можем составить выполнение, выбрав некоторую последовательность единиц выполнения ei (t i , bi ), где
bi из pi так, что инструкции для каждого потока t i расположены в том же порядке, что и для пути pi . Отметим, что выполнение, составленное из путей в потоках, может быть недостижимым в программе. Несколько последовательных инструкций в пути потока может принадлежать взаимоисключающему интервалу (например, блоки синхронизации и синхронизированные методы, реализованные как вход и выход из монитора). К интервалу приписан объект, по которому происходит взаимоисключение. Инструкции, принадлежащие двум взаимоисключающим интервалам с одинаковыми приписанными объектами, в любом достижимом выполнении не пересекаются, т.е. либо сначала выполняются все инструкции первого интервала, а потом второго, либо наоборот.
тоже состояние s ' , т.е.
3. Достаточные условия сериализуемости выполнений Понятие независимости инструкций широко используется при верификации программ. Одно из наиболее распространенных применений — сокращение пространства поиска достижимых состояний или выполнений [6,7,10]. Интуитивно, две инструкции, выполняющиеся в разных потоках, независимы в некотором состоянии, если любое их выполнение этом состоянии приводит к одному и тому же результату. Определение 2. Единицы выполнения программы s , если: 1. В состоянии s ' выполнения e ,e
e j , ei
a1 ,..., an и b1 ,..., bm — пути в двух потоках. Условие выполнено, если существует единственная пара ( ai0 , bi0 ) зависимых
Условие 1. Пусть
инструкций. Все остальные пары
(ai , b j ), (i, j ) (i0 , j0 ) либо независимы
во всех состояниях, либо они зависимы и выполнено: 1.
ai0 , ai — принадлежат одному взаимоисключающему интервалу с
obj1 ; 2. b j 0 , b j — принадлежат одному взаимоисключающему интервалу с
r
obj2 ; 3. obj1 obj2 .
выполнение
r r fˆ (t i1 , bi11 ),..., (t i1 , bi1i1 ),..., (t im , bi1m ),..., (t im , bimim ) такое, что t i1 ,..., t im
75
достижимы или
Если единицы выполнения зависимы (независимы), то говорят, что зависимы (независимы) инструкции в этих выполнениях. Рассмотрим следующее условие.
потоки t1 ,..., t m и пути bi ,..., bi i в каждом потоке t i . Выполнение e1 ,..., en существует
ei , e j и e j , ei
i j 2. Если s s1 и s s2 , то s1 s2 . Иначе единицы выполнения называют зависимыми.
e ,...,e
если
ei , e j независимы в состоянии
недостижимы одновременно;
1 Определение 1. Пусть s n s ' и выполнение e1 ,..., en содержит
сериализуемо,
ˆ
f s s' .
В данной работе мы пытаемся решить следующую задачу: для заданной программы установить, сериализуемы ли все достижимые выполнения этой программы.
2.1. Сериализуемость выполнения 1
fˆ переводит систему в
76
Утверждение 1. Пусть пар путей
p1 , p2 ,..., pk — пути в потоках. Если для любой из
( pi , p j ) выполнено условие 1, то любое достижимое выполнение,
составленное из путей
p1 , p2 ,..., pk , сериализуемо.
Для доказательства данного утверждения достаточно заметить, что в достижимом выполнении независимые инструкции можно переставлять местами, при этом результирующее состояние остается неизменным. Дополнительно отметим, что кроме одной пары, все остальные зависимые инструкции принадлежат взаимоисключающим интервалам с одинаковыми объектами и, следовательно, в любом достижимом выполнении появляются либо сначала зависимые инструкции одного потока, затем второго, либо наоборот. Поэтому перестановкой остальных независимых инструкций получим выполнение, в котором потоки выполняются последовательно. ■ Будем говорить, что путь в потоке достижим, если его можно выделить из какого либо достижимого выполнения. Будем говорить, что множество путей в потоках p1 , p2 ,..., pk исчерпывающее, если оно включает все достижимые пути в потоках, т.е. является аппроксимацией сверху множества всех достижимых путей в потоках. Отметим, что наряду с достижимыми путями оно может включать и недостижимые пути. Следствие.
Заметим,
что
если
в
утверждении 1
исчерпывающее множество путей в потоках, выполнение программы сериализуемо.
p1 , p2 ,..., pk —
то любое достижимое
3.1. Дисциплина синхронизации доступа к разделяемым переменным В данной работе мы считаем, что программа удовлетворят дисциплине синхронизации доступа к разделяемым переменным. Суть этой дисциплины заключается в том, что доступ к любой переменной из разных потоков должен происходить из взаимоисключающих интервалов с хотя бы одним совпадающим объектом. Проверка следования данной дисциплине может осуществляться одним из известных алгоритмов [5,11], например алгоритмом ERASER [11]. Отметим, что алгоритм ERASER может быть применен совместно с предложенным в данной работе методом проверки сериализуемости Sapsan. Следование данной дисциплине позволяет устанавливать независимость инструкций на основе анализа принадлежности взаимоисключающим интервалам и сравнением объектов. Утверждение 2. Две инструкции, которые не принадлежат взаимоисключающим интервалам с одинаковыми объектами, независимы в любых состояниях. 77
Утверждение следует из того, что инструкции осуществляют доступ к разным переменным, согласно предположениям о подчинении дисциплине синхронизации. ■ Вместо независимости инструкций мы можем рассматривать независимость блоков, состоящих из попарно независимых инструкций. Для блоков достаточные условия формулируются аналогично инструкциям.
4. Алгоритмы поиска достижимых выполнений В данном разделе мы рассмотрим известные алгоритмы поиска достижимых выполнений. На входе у алгоритмов задана конфигурация выполнения, включающая начальное состояние и набор потоков с воздействиями на систему. В нашей задаче воздействиями являются вызовы интерфейсных методов с некоторыми значениями параметров. Конфигурация является окружением для программы. Программу с окружением можно запустить в результате получим какое-то достижимое выполнение. В зависимости от чередования инструкций могут получаться разные достижимые выполнения. Все множество достижимых выполнений в заданной конфигурации — это выполнения, полученные при всех возможных чередованиях инструкций потоков. Каждое выполнение имеет результат, представляющий собой результирующее состояние. Особо отметим, что в данной работе мы не выделяем возвращаемые значения методов как отдельную часть результата. Мы считаем, что они включаются в состояние, хотя в практических реализациях их удобнее выделять как отдельные сущности. Кроме того, мы не рассматриваем другие возможные реакции системы: обратные вызовы и сообщения. Достижимых выполнений, как правило, очень много и не все из них требуются для установления корректности. Достаточно ограничиться полным множеством выполнений. Множество выполнений называется полным, если его множество результатов совпадает с множеством результатов всех достижимых выполнений. Если полное множество выполнений получается меньше чем множество всех достижимых выполнений, то говорят о сокращении множества выполнений. Задача алгоритма поиска — построить полное множество выполнений. В данной работе мы используем алгоритмы, описанные в работе Годфруа [8]. Эти алгоритмы существенно используют понятие независимости для сокращения множества выполнений. Различают два класса алгоритмов поиска: 1. Алгоритмы с сохранением состояния; 2. Алгоритмы без сохранения состояния.
78
Для того чтобы осуществлять поиск выполнений, необходимо иметь возможность возвращаться в предыдущее состояние. В существующих алгоритмах это достигается двумя способами. В первом способе, возврат осуществляется восстановлением ранее сохраненного состояния. Он используется в алгоритмах с сохранением состояния. Второй способ, использующийся в алгоритмах без сохранения состояния, заключается в сбросе системы в начальное состояние и перевыполнении до нужного состояния. Для этого требуется наличие соответствующего механизма сброса. Как правило, имея возможность хранить состояния, мы имеем возможность сравнивать их, что может быть использовано для сокращения перебора. Кроме того, сравнение состояний дает возможность обнаруживать циклы. Несмотря на эти преимущества, в данной работе выбран второй способ, потому как хранение состояний, которое потребовалось бы в первом способе обладает рядом недостатков: 1. Требует значительных объемов памяти для хранения состояний 2. Требует моделирования состояний недоступных для чтения. Известные алгоритмы, использующие первый способ, реализованы в виде специальной виртуальной машины [4,9,12], которая хранит пройденные состояния и позволяет управлять последовательностью выполнения инструкций. Эти машины с легкостью справляются со всеми инструкциями Java кода. Однако программы на Java, кроме чистого кода содержат вызовы процедур, реализованных на других языках, называемых внутренними (native) методами. Для этих методов требуется написать модель, сохраняющую и восстанавливающую состояния. На практике программы с внутренними методами встречаются часто. Практически все стандартные библиотеки содержат внутренние методы. Например, библиотека работы с сетью. Кроме того, программы часто пользуются системами, написанными на других языках, например, базами данных.
5. Метод Sapsan На вход методу Sapsan подается программа предоставляющая интерфейс, которая должна следовать дисциплине синхронизации доступа к разделяемым переменным, описанной в Разделе 3.1. Вместе с программой должен быть подготовлен тестовый набор, требования к которому мы рассмотрим ниже. Кроме того инструменту Sapsan могут быть предоставлены эвристики для оптимизации поиска. Схема метода показана на Рис. 1. Метод Sapsan состоит из трех шагов. На первом шаге выполняется инструментация кода программы. До и после элементов синхронизации, необходимых для выделения взаимоисключающих интервалов, вставляются специальные блоки, сохраняющие путь. Так как мы предполагаем, что соблюдается дисциплина синхронизации доступа к разделяемым переменным, то инструментация всех остальных инструкций не требуется. В языке Java 79
элементами синхронизации являются инструкции входа в монитор, выхода из монитора, вызовы синхронизованных методов и методов wait, notify и notify All. На втором шаге выполняются тесты. Так как программа инструментирована, то при выполнении автоматически сохраняется пройденный путь для каждого из потоков. К тестовому набору предъявляются следующие требования: 1. Вызовы интерфейсных методов должны быть разделены по потокам, так чтобы в каждом потоке был только один вызов; 2. Запуск тестового набора строит исчерпывающее множество путей в потоках; 3. В тестовом наборе сохраняется информация о состояниях и потоках для каждого достигнутого пути. Смысл первого требования в том, что каждый путь в потоке должен быть путем для одного интерфейсного метода, и не должен включать инструкции других интерфейсных методов и вспомогательных методов тестовой системы. При выполнении этого условия сериализуемость путей в потоках будет означать сериализуемость интерфейсных методов. Второе требование необходимо для того чтобы утверждать сериализуемость программы в целом на основе проверки достаточных условий сериализуемости для найденных путей (следствие из утверждения 1). На практике, если тестовый набор не обеспечивает построение исчерпывающего множества, метод Sapsan может быть применен, но гарантировать сериализуемость мы можем только для найденных путей в потоках. Для того чтобы проверить построено ли исчерпывающее множество можно использовать алгоритмы статического анализа (будем показано далее). Хранение информации для каждого пути необходимо для того чтобы, вопервых, можно было повторно выполнить поток, который привел к данному пути и, во-вторых, вернуться в состояние, в котором выполнялся данный поток. Информация о том, какой поток и в каком состоянии привели к данному пути, составляет основу для выбора состояния и набора потоков в инструменте Sapsan. На третьем шаге запускается инструмент Sapsan. На входе у него имеется исчерпывающее множество путей в потоках, вместе с сохраненной информацией о том, как они были достигнуты, и эвристики по выбору набора потоков. Инструмент пытается установить сериализуемость для всех найденных пар путей в потоках на основе достаточных условий сериализуемости. Если инструмент выдает ответ «Да», то программа сериализуема. Если ответ «Нет», то программа не сериализуема и инструмент выдает набор потоков, состояние и информацию о выполнении на котором нарушается свойство сериализуемости. Возможен также и третий вариант, когда инструменту не удалось установить сериализуема ли программа. На 80
выходе в этом случае пары путей, для которых не выполнены достаточные условия. Эти подозрительные пары могут быть проанализированы вручную.
кода одного из методов, но межпроцедурный анализ и ссылочный анализ обладают существенным недостатком — модели программы содержат большое количество ложных (недостижимых) путей. В объектноориентированных программах анализ усложняется наличием полиморфизма и наследования. Мы выбрали тестирование, потому что оно не вносит ложных путей, строя только те пути, которые есть в достижимых выполнениях. Но как проверить исчерпываемость построенного с помощью тестирования набора путей, требующуюся в методе Sapsan? В методе Sapsan предлагается для частичного ответа на данный вопрос использовать статический анализ в рамках одного метода. Если метод тестирования достигает все пути по вызовам в потоке, и статический анализ в рамках одного метода показывает, что все пути достигнуты (были пройдены все инструкции синхронизации), то полученный набор путей исчерпывающий.
5.2. Установление сериализуемости Цель третьего шага, на котором мы запускаем инструмент Sapsan — установить сериализуемость для всех пар путей в потоках. Схема работы инструмента показана на Рис. 2. Для найденных путей проверяются достаточные условия сериализуемости. При проверке достаточных условий используется информация о независимости участков кода. Для Java программ по умолчанию зависимыми считаются все взаимоисключающие интервалы с одинаковыми объектами. Все остальные блоки кода считаются независимыми, это свойство следует из условия применения дисциплины синхронизации разделяемых переменных. Если достаточные условия выполнены, следовательно, мы показали, что методы интерфейса сериализуемы. Иначе возможно три варианта: 1. В системе присутствует путь, на котором нарушается сериализуемость; 2. Наша информация о независимости блоков кода неполна; 3. Невозможно установить сериализуемость, используя достаточные условия.
Рис. 1. Схема метода Sapsan. Теперь рассмотрим основные некоторые моменты метода более детально.
5.1. Построение потоках
исчерпывающего
множества
путей
в
Цель первых двух шагов метода — построить исчерпывающее множество путей в потоках. В методе Sapsan основным средством его построение является запуск тестов. Возможны и другие способы вычисления путей в потоках, например, один из наиболее распространенных — статический анализ. Статический анализ — это построение модели программы и анализ ее свойств без запуска программы. Он хорошо зарекомендовал себя при анализе 81
В первом случае нам необходимо попытаться найти достижимое выполнение не являющееся сериализуемым. Во втором попытаться установить независимость блоков кода. В инструменте Sapsan в обоих случаях в качестве основы используется алгоритм поиска достижимых выполнений без сохранения состояний (см. раздел 4) [8], реализованный в инструменте VeriSoft. Этот алгоритм был оснащен возможностью поиска по шаблону, что позволило сократить пространство поиска. Шаблон задает порядок, в котором должны встретиться единицы выполнения в искомом выполнении. Шаблоны строятся на основе пар путей, для которых не выполнено Условие 1 раздела 3. Поиск по шаблону 82
нацеливается только на выполнения удовлетворяющие шаблону, т.е. выполнения, не подходящие под шаблон, отбрасываются.
Поиск несериализуемых путей требует наличия механизма проверки сериализуемости для пути выполнения. В методе Sapsan проверка происходит сравнением результата выполнения с результатами последовательного прогона потоков. Отметим, что возможны и другие способы проверки. Например, если в тестовом наборе уже описана модель требований, обладающая сериализуемостью, то можно ее переиспользовать. Так, например, в технологии UniTESK [1,3], зачатую ее бывает выгоднее переиспользовать.
5.2.2. Эвристический метод установления независимости Эвристический метод установления независимости требует механизма сравнения результирующих состояний выполнений.
наличия
Для того чтобы установить независимость блоков b1 ,b2 в потоках t1 ,t2 соответственно, мы делаем следующее. Осуществляем поиск по шаблону, находим выполнения, в которых встречаются последовательно единицы выполнения (t1 , b1 ), (t 2 , b2 ) и (t2 , b2 ), (t1 , b1 ) и запоминаем результирующие состояния. Дальше для всех пар выполнений, которые отличаются перестановкой единиц выполнения, сравниваем результирующие состояния. Если на всех таких выполнениях результаты совпадают, то считаем, что b1 ,b2 независимы, иначе зависимы. То есть если для любых двух выполнений:
e1 ,..., ek , (t1 , b1 ), (t2 , b2 ), ek 1 ,..., en с результатом r1 и 2. e1 ,..., ek , (t2 , b2 ), (t1 , b1 ), ek 1 ,..., en с результатом r2 выполнено r1 r2 , то b1 ,b2 независимы. 1.
Рис. 2. Установление сериализуемости.
5.2.1. Поиск несериализуемых выполнений Если для двух путей p1 , p2 не выполнено Условие 1, то существует четверка блоков u1 , u2 , v1 , v2 такая, что блоки u1 , u 2 взяты из пути потока p1 , v1 , v2 взяты из пути потока p2 , u1 , v1 — зависимы, u2 , v2 — зависимы, и: 1.
либо u1 ,u 2 не принадлежат одному взаимоисключающему интервалу,
2.
либо v1 , v2 не принадлежат одному взаимоисключающему интервалу,
3.
либо u1 ,u 2 и v1 , v2 принадлежат одним взаимоисключающим интервалам одновременно, но с разными объектами.
Отсюда получаем, что возможно четыре шаблона: u1 , v1 , u2 , v2 , u1 , v1 , v2 , u2 ,
v1 , u1 , u2 , v2 , v1 , u1 , v2 , u2 . 83
5.2.3. Эвристики по выбору набора потоков Везде, где мы осуществляем поиск, требуется задать конфигурацию (см. Раздел 4), т.е. нужно задать потоки и состояние, в котором будет осуществлен поиск. Мы имеем два пути p1 , p2 для которых не выполнены достаточные условия, и шаблон по которому производиться поиск. От тестового набора, запускаемого на шаге 2, мы потребовали сохранения для каждого пути информации о состоянии si и потоке ti . Используя эту информацию, мы можем запускать соответствующие потоки и переходить в соответствующие состояния. В общем случае ответить на вопрос, достижимо данное выполнение, невозможно. Поэтому задача выбора набора потоков и состояния для достижения заданного выполнения неразрешима. Однако, исходя из 84
сохраненной информации для путей p1 , p2 , мы можем предложить следующие эвристики, перечисленные в порядке приоритетности их применения. 1.
Рассматриваем пары потоков, сохраненные для путей p1 , p2 , и пересечение состояний для путей p1 , p2 .
2. 3. 4.
То же самое, только берем объединение состояний для путей p1 , p2 . Добавляем другие потоки. Добавляем другие состояния.
На практике мы осуществляем поиск лишь для небольшого числа потоков. В результате экспериментов мы установим, насколько часто удается установить независимость или найти ошибку. Мы надеемся, что ситуация, при которой мы не можем достоверно сказать есть ли ошибка или нет, на практике будет встречаться редко. Отметим, что даже когда этого нельзя достоверно утверждать для всех выполнений, в процессе поиска мы проверяем сериализуемость вплоть до некоторого количества потоков.
6. Результаты применения метода Для апробации инструмента Sapsan в качестве тестового набора использовались тестовые варианты JUnit. Каждый тестовый вариант testCase состоит из одного вызова интерфейсного метода с некоторыми параметрами. Для тестового варианта задан метод setUp — инициализации состояния и метод tearDown — приведения системы в начальное состояние. Для всех тестовых вариантов задан метод getState, вычисляющий текущее состояние системы. Таким образом, запуск тестового варианта состоит из последовательности: setUp, начало сбора пути в потоке, testCase, конец сбора пути в потоке, сохранить информацию, getState, tearDown. На шаге 3 использовались эвристики 1 и 2 (см. 5.2.3). Проверка сериализуемости выполнения происходит сравнением результата выполнения с результатами последовательных выполнений. Были написаны тесты для реализаций кэша Ehcache [2], который оптимизирует доступ к хранящимся в нем элементам, размещая часто используемых из них в памяти, а остальные сохраняя на диске. Реализация этого кэша составляет примерно 40 тысяч строк кода на Java. Кэш предоставляется интерфейс, состоящий из трех основных методов: 1. put — положить элемент в кэш; 2. get — считать элемент из кэша; 3. remove — удалить элемент из кэша. Для каждого из методов было написано по нескольку тестовых вариантов. Проведенные запуски инструмента Sapsan показали следующее: 85
1.
Пути в потоках не зависят от набора потоков и слабо зависят от состояний. Получается, что исчерпывающее множество путей, можно получить при последовательном выполнении потоков в небольшом наборе состояний.
2.
Было выявлено два нарушения достаточных условий. Для первого из них было найдено несериализуемое выполнение, а для второго была установлена независимость блоков эвристическим методом. Причем в конфигурациях использовались только пары потоков и пересечение состояний (эвристика 1).
Кроме того, эксперименты показали, что исчерпывающий набор путей может быть получен уже при последовательном выполнении потоков.
7. Заключение В работе описан метод Sapsan, который позволяет проверять сериализуемость программы. Если результатом применения метода является ответ «Да», то программа гарантированно является сериализуемой. Это отличает данный метод от методов проверки моделей, которые могут гарантируют сериализуемость лишь для конечного числа потоков. Кроме того за счет применения алгоритмов поиска лишь для некоторых пар путей и поиска по шаблону метод значительно выигрывает у них в скорости. Метод был успешно опробован на достаточно большой реализации кэша. В дальнейшем планируется применить метод для ряда других реализаций. Литература [1] В. В. Кулямин, А. К. Петренко, А. С. Косачев, И. Б. Бурдонов. Подход UniTESK к разработке тестов. Программирование, 29:25–43, 2003. [2] http://ehcache.sourceforge.net. [3] А. В. Хорошилов. Спецификация и тестирование компонентов с асинхронным интерфейсом. Диссертация на соискание ученой степени кандидата физикоматематических наук. 2006. [4] D. Bruening. Systematic testing of multithreaded Java programs, 1999. [5] T. Elmas, S. Qadeer, and S. Tasiran. Goldilocks: a race and transaction-aware Java runtime. In Proceedings of the 2007 ACM SIGPLAN conference on Programming language design and implementation, pp. 245–255, NY, USA, 2007. [6] C. Flanagan and P. Godefroid. Dynamic partial-order reduction for model checking software. In POPL '05: Proceedings of the 32-nd ACM SIGPLAN-SIGACT symposium on Principles of programming languages, pp. 110–121, NY, USA, 2005. [7] P. Godefroid. Partial-Order Methods for the Verification of Concurrent Systems: An Approach to the State-Explosion Problem. Springer-Verlag New York, Inc., Secaucus, NJ, USA, 1996. Foreword By-Pierre Wolper. [8] P. Godefroid. Model Checking for Programming Languages using Verisoft. In Symposium on Principles of Programming Languages, pp. 174–186, 1997.
86
[9] V. S. Mutilin. Concurrent testing of Java components using Java PathFinder. Proceedings of ISoLA 2, 2006. [10] D. Peled. Combining partial order reductions with on-the-fly model checking. In Formal Methods in System Design, vol. 8, pp. 39–64, 1996. [11] S. Savage, M. Burrows, G. Nelson, P. Sobalvarro, and T. Anderson. Eraser: A dynamic data race detector for multithreaded programs. ACM Transactions on Computer Systems, 15(4):391–411, 1997. [12] W. Visser, K. Havelund, G. Brat, S. Park, and F. Lerda. Model checking programs. Automated Software Engg., 10(2):203–232, 2003.
87
Критерии тестового покрытия, основанные на структуре контрактных спецификаций В. В. Кулямин
[email protected] Аннотация. В данной статье рассказывается о критериях тестового покрытия, применяемых в технологии UniTESK и основанных на структуре контрактных спецификаций функциональных требований. Эти критерии можно использовать при тестировании с использованием контрактных спецификаций (пред- и постусловий операций), независимо от применяемой при этом технологии. Приводится алгоритм определения достижимых комбинаций элементарных условий из кода пред- и постусловий, используемый для автоматического удаления недостижимых ситуаций из отчетов о тестовом покрытии.
1. Введение Одним из наиболее широко используемых методов контроля качества программных и аппаратных систем является тестирование. Оно определяется как проверка соответствия поведения системы требованиям к ней, выполняемая на основе результатов работы этой системы в некотором конечном наборе специально созданных ситуаций [1]. В этом определении подчеркивается, что тестирование является конечной процедурой, оно всегда включает конечное число экспериментов с тестируемой системой или тестов. Однако практически значимые системы настолько сложны, что количество различных ситуаций, которые необходимо протестировать для полной проверки одной такой системы, превосходит возможности сколь угодно щедро финансируемого проекта и требует колоссального времени для выполнения. Поэтому исчерпывающее тестирование, хотя и возможно теоретически из-за физической конечности любой вычислительной системы, практически совершенно невыполнимо. Этот факт зафиксирован в известной фразе Дейкстры: «Тестирование может быть использовано для демонстрации наличия ошибок, но никогда — для доказательства их отсутствия» [2]. Тем не менее, на практике именно тестирование чаще всего служит основой для оценки качества сложных систем, в том числе, и для определения того, насколько много в них ошибок, включая невыявленные. Есть ли какие-либо 89
основания для этого? Большинство инженеров считают, что за счет аккуратного и систематичного выбора тестовых ситуаций — входных данных, сценариев выполняемых действий и конфигураций тестируемой системы, используемых в них, — можно обеспечить достаточно высокий уровень доверия к результатам тестирования. То есть, аккуратный выбор небольшого конечного набора тестов позволяет ему адекватно отражать качество системы в целом, или, другими словами, стать статистически представительной выборкой из огромного множества всех ситуаций, в которых система должна функционировать. Набор правил, следуя которому производится систематический отбор конечного числа тестов, принято называть критерием полноты тестирования (или критерием его адекватности) [3]. Определение подходящего для тестируемой системы критерия полноты является одной из важнейших задач, которые нужно решить при организации тестирования, наряду с определением требований к тестируемой системе. Чаще всего критерий полноты основывается на разбиении всех ситуаций, в которых система должна работать, на классы эквивалентности на основе некоторых предположений о том, что в эквивалентных ситуациях система работает примерно одинаково, и поэтому либо в обеих присутствует ошибка, либо, наоборот, в обеих ошибки нет. Такие критерии полноты называются критериями тестового покрытия, а полнота тестирования с их помощью измеряется как достигнутое тестовое покрытие — процент классов эквивалентных ситуаций, задействованных в ходе тестирования, по отношению к числу всех классов. Эта метрика основана на простой эвристике — чем больше неэквивалентных, «существенно различающихся» ситуаций проверено, тем полнее было тестирование, и тем лучше оно отражает реальное качество системы. Предположения, на основании которых выбираются критерии покрытия, могут иметь различную природу. Обычно используются два вида предположений — различные ситуации, возникающие при работе тестируемой системы, объявляются эквивалентными либо, исходя из того, что к ним относится один и тот же набор утверждений в требованиях к системе, либо на основе того, что в них задействован один и тот же набор элементов самой системы. Критерии первого вида называются функциональными, второго вида — структурными. Функциональные критерии покрытия удобны для проверки соответствия поведения системы требованиям — при их использовании тесты нацеливаются на проверку требований, иногда в различных нетривиальных сочетаниях, и достаточно легко можно обнаружить, что какие-то требования совсем не реализованы. Структурные критерии покрытия удобны для тестирования отдельных модулей или сценариев взаимодействия небольшого числа модулей, их использование также позволяет выявить недостижимый код и участки кода с неясным предназначением, подозрительные как с точки 90
зрения наличия в них ошибок, так и с точки зрения защищенности тестируемой системы и безопасности ее работы. Часто на практике используется некоторая комбинация из функциональных и структурных критериев покрытия, поскольку в сочетании они компенсируют недостатки друг друга, сохраняя достоинства. При создании технологий и инструментов автоматизации тестирования используемые в них критерии тестового покрытия играют важную роль, определяя как отдельные элементы этих инструментов и технологий, так и границы их применимости. В разработанной в Институте системного программирования РАН технологии тестирования UniTESK [4-6] используются, в основном, функциональные критерии покрытия. Эти критерии определяются на основе структуры формальных спецификаций функциональных требований к тестируемой системе, являющихся основным источником тестов в рамках этой технологии, и поэтому, являясь функциональными по существу, по форме они очень похожи на структурные. Тестовые ситуации объявляются эквивалентными в их рамках, если в этих ситуациях задействован один и тот же набор элементов спецификаций, представляющих требования к тестируемой системе. Данная статья представляет применяемые в UniTESK критерии покрытия, которые с равным успехом могут использоваться и в других подходах, где возникают контрактные спецификации в виде пред- и постусловий операций, использующих модельные состояния описываемой системы. В статье также описывается алгоритм определения классов эквивалентных ситуаций по отношению к самому детальному из используемых критериев — критерию комбинаций логических условий в спецификациях. Кроме того, обсуждаются возможные варианты развития механизмов описания и использования критериев покрытия в тестах, создаваемых с помощью UniTESK.
2. Критерии покрытия в UniTESK Прежде, чем приступить к описанию применяемых в UniTESK критериев покрытия, стоит рассмотреть общую структуру спецификаций, используемых в этой технологии.
2.1. Структура формальных спецификаций UniTESK В технологии UniTESK для описания требований к тестируемой системе используются формальные контрактные спецификации [4-6]. Это означает, что ими формально фиксируется синтаксис интерфейса тестируемой системы — названия операций и типы параметров и результатов для каждой из них, а также программный контракт каждой из операций [7]. Программный контракт операции состоит из предусловия и постусловия. Первое фиксирует требования к корректному использованию этой операции со стороны окружения системы — при каких ограничениях на аргументы 91
обращение к этой операции корректно. Второе определяет обязательства системы по отношению к результатам вызовов этой операции — какие ограничения на результаты работы операции должны быть выполнены при корректной работе системы, если обращение к ней было правильным. При нарушении предусловия операции обращение к ней может иметь любые последствия, поведение системы в этом случае не определено. Совместно пред- и постусловия строго определяют требования к работе системы. При анализе сложной системы с большим количеством интерфейсных операций крайне неудобно рассматривать их все вместе как однородный набор, необходимо разбиение этих операций на какие-то логические группы по реализуемым ими функциям. Эти группы почти всегда соответствуют компонентам или модулям системы, ответственным за реализацию этих функций. Часто такие компоненты имеют внутреннее состояние, недоступное для непосредственного наблюдения извне системы, но влияющее на поведение вызываемых операций компонента и изменяемое ими. Поэтому для описания ограничений в пред- и постусловиях необходимо както учитывать внутреннее состояние компонента, операции которого описываются. Чтобы сделать это возможным, контракты операций одного компонента объединяются в спецификацию этого компонента вместе с описанием структуры его модельного состояния. Структура модельного состояния компонента не обязана совпадать со структурой его реального состояния, зафиксированной в его коде, например, компонент, реализующий список, может использовать ссылочную структуру данных, в том время, как структура его модельного состояния в спецификациях может быть основана на массиве. В структуре модельного состояния могут совсем отсутствовать некоторые части реального состояния компонента, слабо связанные с его функциональностью, например, различного рода кэши и другие данные, используемые для увеличения производительности. Важно, что структура модельного состояния компонента должна содержать данные, достаточные для полного описания функциональности в пред- и постусловиях операций этого компонента. При этом предусловие зависит от аргументов вызова операции и данных модельного состояния при этом вызове, или пре-состояния. Постусловие зависит как от аргументов и пре-состояния, так и от результата и данных модельного состояния после вызова, или пост-состояния. Наличие сложной структуры модельного состояния приводит к необходимости учитывать в контрактах операций ограничения на его данные, которые должны выполняться в стабильных состояниях системы, когда ни одна из операций не выполняется. Такие ограничения оформляются в виде инвариантов модельного состояния. Инварианты являются общими частями пред- и постусловий всех операций, как-то затрагивающих соответствующие компоненты — они должны выполняться как в пре-состоянии любого вызова, так и в его пост-состоянии. 92
Предусловия, постусловия и инварианты в UniTESK оформляются на расширении одного из языков программирования. Каждая из этих конструкций представляет собой блок кода, возвращающий значение булевского типа и помеченный для наглядности специальным ключевым словом, соответственно, pre, post или invariant.
2.2. Критерии тестового покрытия на основе структуры пред- и постусловий Самым простым из используемых на практике является критерий покрытия операций, в рамках которого эквивалентными считаются все обращения к одной и той же операции. Этот критерий наименее детальный, и стопроцентное покрытие согласно ему гарантирует только минимальное качество тестов — каждая тестируемая операция вызвана хотя бы один раз. Можно заметить, что определение этого критерия никак не связано с использованием спецификаций или требований. Более детальные критерии покрытия на основе контрактных спецификаций уже опираются на их структуру. Пред- и постусловия операций представляют собой запись требований к описываемой ими системе, поэтому критерии покрытия, основанные на их структуре являются функциональными. С другой стороны, пред- и постусловия можно рассматривать как некоторый код и определять критерии его покрытия таким же образом, как это делается для кода программ. Обычно постусловие операции описывает несколько разных режимов ее функционирования. Предусловие запрещает все комбинации значений параметров, при которых поведение операции не определено. Однако среди разрешаемых предусловием ситуаций могут остаться такие, в которых выполнить свою основную функцию операция не может. Вместо этого она должна вернуть некоторый код ошибки, создать исключительную ситуацию или как-то иначе сигнализировать о том, что ее основная задача не может быть решена в такой ситуации. Помимо режимов сигнализации о невозможности выполнить основную функцию, само ее выполнение может идти разными путями. Например, операция конкатенации двух строк может проверять, не является ли один из ее аргументов пустой строкой, и в этом случае просто возвращать копию другого аргумента, не выполняя никаких действий над ней. Если же оба аргумента не пусты, копия одного из них должна быть подвергнута модификации, чтобы получить результат операции. При написании постусловий удобно выделять такие разные режимы, иначе постусловие превращается в большую и сложную формулу, трудную для анализа и понимания, что противоречит основной цели создания формальных спецификаций. Различные режимы работы операции описываются в виде импликаций с несовместными посылками (постусловие при этом выглядит как формула X1 Y1 & … & Xn Yn) или в виде разных выражений для возвращаемого результата при разных исходных условиях, т.е. результат 93
постусловия вычисляется в разных ветвях некоторого условного оператора или оператора выбора по-разному. Поэтому эти разные режимы называют ветвями функциональности (или функциональными ветвями) операции. Соответственно, следующим в сторону повышения детальности после критерия покрытия операций является критерий покрытия ветвей функциональности. Он объявляет эквивалентными все вызовы одной операции, в которых выполняется одна и та же функциональная ветвь. В инструментах UniTESK, основанных на использовании расширений различных языков, используются несколько разные конструкции для выделения функциональных ветвей. В расширении Java вводится оператор branch, который помечает ветвь условного оператора или оператора выбора в коде постусловия, соответствующую ветви функциональности и определяет идентификатор этой ветви функциональности. Поскольку критерии тестового покрытия в UniTESK используются во время выполнения тестов для фильтрации тестовых данных и отбора только таких вызовов, которые повышают уже достигнутое покрытие, на размещение операторов branch наложены ограничения. Они позволяют генерировать фильтры, которые без обращения к самой тестируемой операции могут определить ветвь функциональности, соответствующую произвольному набору ее аргументов. Ограничения состоят в следующем. Все ветвления на пути по потоку управления от начала постусловия до любого из операторов branch должны зависеть только от аргументов вызова и данных пре-состояния. При соблюдении этого условия ветвь функциональности можно вычислить, не вызывая саму операцию. По сути, данные пост-состояния могут появиться в постусловии только после одного из операторов branch, поэтому такие операторы помечают в постусловии места, в которых вызывается тестируемая операция во время работы теста. На любом пути по потоку управления от начала постусловия до выхода из него должен быть ровно один из операторов branch. Это позволяет отнести каждый вызов, удовлетворяющий предусловию к одной и только одной из функциональных ветвей. В расширении языка C, используемом для разработки тестов с помощью технологии UniTESK, ветви функциональности определяются в блоке coverage, отделенном от постусловия. Такие блоки используются для вычисления классов эквивалентности тестовых ситуаций по разным критериям, в том числе и по критерию покрытия функциональных ветвей. Все другие критерии покрытия, используемые в UniTESK, являются более детальными, чем критерий покрытия ветвей функциональности. В порядке увеличения детальности они приведены ниже.
94
постусловия до выхода из него стоит ровно один оператор branch. Можно отметить также, что каждый определяющий путь однозначно определяет лежащую на нем цепочку меток, которая заканчивается оператором branch. Критерий покрытия определяющих путей считает эквивалентными вызовы операции, приводящие к прохождению одного и того же определяющего пути. Этот критерий покрытия не менее детален, чем критерий покрытия помеченных путей. Ближайшим аналогом этого критерия является критерий покрытия ветвей исходного кода (branch coverage или decision coverage) [3,8]. В его рамках эквивалентными являются обращения к операции, при которых выполняется один и тот же набор ветвей во всех условных операторах, операторах выбора или операторах цикла. Определяющий путь тоже соответствует набору ветвей в постусловии, без рассмотрения циклов и всего кода в них, а также без учета кода, выполняемого после операторов branch.
Критерий покрытия помеченных путей. Операторы branch, определяющие ветви функциональности, по сути являются специальными метками, с помощью которых в коде постусловия выделяются классы эквивалентности вызовов соответствующей операции. Расстановку меток можно использовать и для более гибкого определения покрытия. Для этого используются метки в виде операторов mark с идентификатором данной метки. Такие метки можно использовать как в предусловии, так и в постусловии, с единственным ограничением — не ставить их внутри циклов. При следовании ему существует лишь конечное множество цепочек меток, которые могут быть пройдены при выполнении проверок предусловия и постусловия. Можно рассматривать как эквивалентные вызовы, которые приводят к прохождению одной и той же такой последовательности. Однако в UniTESK класс эквивалентности вызова должен быть вычислим без обращения к самой тестируемой операции, поэтому такие цепочки нужно обрывать при прохождении оператора branch. Критерий покрытия помеченных путей рассматривает как эквивалентные вызовы операции, приводящие к прохождению одной и той же цепочки операторов mark в пред- и постусловии этой операции, заканчивающейся оператором branch. Таким образом, классы вызовов по этому критерию вычислимы без обращения к операции, а сам критерий является не менее детальным, чем критерий покрытия ветвей функциональности.
Критерий покрытия определяющих путей. Рассмотрим граф потока управления предусловия, в котором все циклы стянуты в вершины, т.е. все вершины графа, входящие в один и тот же цикл, отождествлены в одну вершину, все ребра, входящие в них, входят в эту вершину, все выходящие — выходят из нее, а все ребра, ранее входившие в состав циклов, отброшены. В получаемом таким образом графе нет циклов, а значит количество путей, ведущих из его стартовой вершины, соответствующей началу предусловия, в одну из конечных вершин, соответствующих выходу из предусловия, конечно. То же самое верно и для графа потока управления постусловия, в котором все циклы стянуты в вершины, и все пути оборваны на операторах branch. Определяющим путем называется путь по графам потока управления предусловия и постусловия со стянутыми в вершины циклами, который оканчивается оператором branch и проходится при выполнении проверок при одном из вызовов операции. Из сказанного выше следует, что существует лишь конечный набор определяющих путей, и любой вызов операции соответствует одному из них, поскольку на любом пути по потоку управления от начала 95
96
Критерий покрытия комбинаций элементарных условий. Элементарным условием в данной статье будем называть логическое выражение, являющееся частью условия одного из условных операторов или операторов выбора в пред- или постусловии операции на определяющем пути, которое неразложимо на более мелкие логические выражения. Значения элементарных условий определяют выбор ветвей в условных операторах и операторах выбор, а значит и путь по потоку управления пред- и постусловия, проходимый при этих значениях. Полной комбинацией элементарных условий мы называем набор значений элементарных условий, в котором каждому условию сопоставлено либо значение true («истина»), либо false («ложь»), либо неопределенное значение, причем имеющийся набор определенных значений (true и false) однозначно задает некоторый определяющий путь в пред- и постусловии операции, и все определенные значения хотя бы один раз вычисляются при прохождении этого пути. Последнее ограничение означает, что неопределенное значение могут иметь те и только те элементарные условия, значения которых не влияют на выбор пути. Произвольный набор значений элементарных условий накрывает данную полную комбинацию, если значения, определенные в этой комбинации, совпадают со значениями, определенными в этом наборе. Критерий покрытия комбинаций элементарных условий объявляет эквивалентными вызовы одной и той же операции, при которых наборы значений элементарных условий в пред- и постусловии накрывают одну и ту же полную комбинацию. Поскольку элементарных условий конечное множество, их полных комбинаций
также конечное множество, причем каждая полная комбинация однозначно задает определяющий путь, хотя некоторые различные комбинации могут задавать один и тот же определяющий путь (из-за использования дизъюнкций нескольких элементарных условий в одном условии ветвления). Это означает, что критерий покрытия комбинаций элементарных условий не менее детален, чем критерий покрытия определяющих путей. Аналогичный критерий покрытия кода называется критерием покрытия комбинаций условий (multiple condition coverage) [3,9]. Однако, согласно определению в его рамках эквивалентны такие ситуации, в которых все элементарные условия, используемые в ветвлениях, имеют в точности одни и те же значения. Использование неопределенных значений обычно игнорируется, хотя на практике оно необходимо. Например, если условие имеет вид (object != null) && object.isCertain(), в него входят два элементарных условия — object != null и object.isCertain() — причем, если первое имеет значение false, то значение второго не определено и не может быть определено корректно и осмысленно. Наиболее естественное расширение определения этого критерия, допускающее неопределенные значения, все-таки несколько отличается от данного выше для критерия комбинаций условий в спецификациях. Первое, в отличие от второго, использует значения всех формул, которые можно корректно определить, а не только тех, которые необходимы для вычисления пути по графу потока управления. В первом случае неопределенные значения могут оставаться только у таких условий, для которых вычисление значений требует, чтобы другие условия имели конкретные значения, как в приведенном примере с условиями object.isCertain() и object != null.
В приведенном примере есть два разных режима работы операции изменения баланса. В одном режиме возможное переполнение или нарушение ограничения, требующего, чтобы текущее значение баланса было не меньше минимального разрешенного его значения, приводят к тому, что состояние счета не изменяется и возвращается результат false; проверки, соответствующие этому режиму помечены оператором branch NoChanges. В другом режиме никакие ограничения не нарушаются, и переполнение не происходит, поэтому текущее значение баланса изменяется на величину параметра операции и возвращается результат true; этот режим помечен оператором branch NormalCase. specification class AccountSpecification { int balance = 0; int minBalance = 0; invariant balanceNotLessThanMinimum() { return balance >= minBalance; } specification boolean deposit(int s) { post { tautology (balance >= minBalance) && (s >= 0 || Integer.MIN_VALUE - s <= 0); if(s > 0 && Integer.MAX_VALUE - s < balance) mark "Overflow"; else if( s < 0 && minBalance < 0 && minBalance - s > balance || s < 0 && minBalance >= 0 && minBalance > balance + s ) mark "Under minimum"; else { branch NormalCase;
2.3. Пример использования критериев покрытия Для пояснения определений из предыдущего раздела, рассмотрим класс, реализующий функции простого банковского счета с изменяемым минимальным разрешенным значением баланса. Такой счет должен иметь текущее значение баланса и минимальное разрешенное значение баланса; для простоты будем считать оба этих значения целыми числами, представимыми типом int в языке Java. У рассматриваемого класса имеются операции изменения минимального разрешенного значения баланса и изменения текущего значения баланса. Ниже приведены описание структуры модельного состояния для такого класса и контрактная спецификация второй операции на расширении языка Java, используемом в UniTESK. 97
return
} 98
balance == pre balance + s && minBalance == pre minBalance && deposit == true;
определяющий путь проходит через уникальный набор меток. Однако в общем случае критерий покрытия определяющих путей оказывается более детальным, чем критерий покрытия помеченных путей.
if(s < 0 && Integer.MIN_VALUE - s > balance) mark "Underflow"; branch NoChanges; return
2.4. Недостижимые ситуации
balance == pre balance && minBalance == pre minBalance && deposit == false;
} } } Кроме того, с помощью операторов mark выделены особые ситуации: переполнение с переходом через максимальное значение целого числа в Java (Overflow), переполнение с переходом через минимальное значение целого числа в Java (Underflow) и возможное нарушение ограничения на минимальное текущее значение баланса (Under minimum). Классы эквивалентности вызовов, определяемые для операции изменения текущего значения баланса критериями покрытия, описанными в предыдущем разделе, приведены в Таблице 1. Полные условия, соответствующие определяющим путям, и элементарные формулы приведены в Таблице 2. Знаки ‘+’ и ‘–’ в Таблице 1 обозначают значения элементарных условий true и false, соответственно. Пустая клетка в строке комбинации обозначает, что соответствующее элементарное условие в данной комбинации имеет неопределенное значение. Ветви функциональности NormalCase
NoChanges
Помеченные пути
NormalCase Overflow; NoChanges Under minimum; NoChanges Under minimum; Underflow; NoChanges
Определяющие пути P1 P2 P3
P4
Комбинации элементарных условий С1
С2
– – + +
– + – –
–
+
С3
С4
С5
– +
–
С6
С7
– –
+
+
–
–
+
–
+
+
+
– –
+
–
+
+
+
В Таблице 1 перечислены только достижимые, встречающиеся на практике классы эквивалентности по различным критериям. В принципе, из управляющих конструкций кода постусловия можно сделать вывод, что возможна последовательность меток Overflow; Underflow; NoChanges. Недостижимость ее следует из того, что условие прохода через метку Overflow включает s > 0, а условие прохода через Underflow — s < 0. Условия выполнения определяющих путей !(0 < s && Integer.MAX_VALUE – s < balance) && !(s < 0 && minBalance < 0 && balance < minBalance – s P1 || s < 0 && !(minBalance < 0) && balance + s < minBalance) 0 < s && Integer.MAX_VALUE – s < balance && !(s < 0 P2 && balance < Integer.MIN_VALUE – s) !(0 < s && Integer.MAX_VALUE – s < balance) && s < 0 && minBalance < 0 && balance < minBalance – s || s < 0 && !(minBalance < 0) && balance + s < P3 minBalance && !(s < 0 && balance < Integer.MIN_VALUE – s) !(0 < s && Integer.MAX_VALUE – s < balance) && s < 0 && minBalance < 0 && balance < minBalance – s || s < 0 && !(minBalance < 0) && balance + s < P4 minBalance && s < 0 && balance < Integer.MIN_VALUE – s Элементарные условия s < 0 C1 0 < s C2 Integer.MAX_VALUE – s < balance C3 minBalance < 0 C4 balance < minBalance – s C5 balance + s < minBalance C6 balance < Integer.MIN_VALUE – s C7
Таблица 1. Классы эквивалентности, выделяемые описанными критериями покрытия в примере.
Таблица 2. Условия определяющих путей и элементарные условия в примере.
В рассматриваемом примере критерии покрытия помеченных и определяющих путей дают одни и те же классы эквивалентности — каждый 99
Более тяжело определить недостижимость комбинации элементарных условий C1 = true, C2 = false, C4 = false, C6 = true, C7 = true. При этих условиях 100
выполнено s < 0, minBalance >= 0, balance < Integer.MIN_VALUE – s. Из первого условия и того, что Integer.MIN_VALUE является наименьшим возможным значением типа int, следует, что Integer.MIN_VALUE – s <= 0; из этого и условия С7 — что balance < 0, а отрицание условия С4 вместе с последним следствием дают balance < minBalance, что прямо нарушает инвариант. Таким образом, указанная комбинация значений элементарных условий действительно недостижима. Для построения корректных тестовых отчетов необходимо выделять недостижимые среди классов эквивалентности, задаваемых критериями тестового покрытия. Эти классы не должны учитываться в отчетах, и итоговое тестовое покрытие должно вычисляться на основе только достижимых классов эквивалентности. Определить недостижимость некоторой ситуации на практике всегда возможно — можно придумать искусственные примеры, в которых это невозможно из-за связи этих примеров с неразрешенными математическими проблемами, однако в реальных системах такие ситуации не встречаются. Однако делать это вручную для сложных систем, имеющих тысячи интерфейсных операций и десятки, а то и сотни неэквивалентных видов ситуаций для каждой операции, крайне трудоемко. В то же время, если недостижимые классы будут присутствовать в отчетах о тестировании, многие тесты не будут давать 100% покрытие, хотя улучшить их не будет никакой возможности. Поэтому необходимо иметь средства для автоматического устранения недостижимых классов ситуаций из отчетов о тестовом покрытии. В инструментах UniTESK, поддерживающих разработку тестов на расширении Java такие средства есть. В частности, для указания недостижимости указанной выше комбинации элементарных условий в нашем примере использована тавтология tautology (balance >= minBalance) && (s >= 0 || Integer.MIN_VALUE - s <= 0). Тавтологии указывают логические выражения, которые истинны в силу смысла используемых в них переменных и операций, а не в силу своей синтаксической структуры. Они используются инструментом построения тестов, чтобы отсекать невыполнимые комбинации условий. Помимо тавтологий, указанных пользователями, инструмент использует естественные тавтологии, отражающие базовые свойства операций сравнения для типов в Java, например, для любых выражений x и y верно (x == y) || (x != y), при любых числовых выражениях x, y, z выполнено !(x < y && y < z && z < x). Тавтологии такого типа не нужно вписывать в спецификации. Достижимость различных помеченных или определяющих путей в UniTESK вычисляется на основе достижимости соответствующих комбинаций значений элементарных условий. Для этого применяется алгоритм, описанный ниже.
2.5. Определение достижимых комбинаций элементарных условий Приводимый в этом разделе алгоритм используется для вычисления набора достижимых комбинаций значений элементарных условий, извлеченных из пред- и постусловия одной операции. Извлеченные из кода спецификаций элементарные условия считаются одинаковыми, если они совпадают текстуально. Такой способ их отождествления накладывает следующие ограничения на написание пред- и постусловий. Один и тот же идентификатор, используемый в условиях разных ветвлений, должен обозначать одно и то же. Это ограничение должно выполняться как для используемых идентификаторов полей классов, так и для локальных переменных. Поэтому нельзя определять в разных частях постусловия локальные переменные с одним и тем же идентификатором, и использовать их в условиях ветвлений или операторах выбора. Кроме того, если переменная используется в условиях нескольких различных ветвлений, ее значение между использованиями не должно изменяться. Иначе одинаковые текстуально условия (x > 0) будут означать разные ситуации. Алгоритм использует список вхождений элементарных условий в пред- и постусловия, в котором вхождения перечисляются в том же порядке, что и в условиях ветвлений или операторах выбора в коде предусловия и постусловия. Заметим, что одно и то же элементарное условие может входить в него несколько раз, поскольку несколько раз может встречаться в разных местах кода. Каждому вхождению условия на каждом шаге алгоритма присваивается одно из значений true или false. Список текущих значений вхождений элементарных формул будем называть таблицей значений, номер вхождения в этом списке — индексом этого вхождения. На каждом шаге алгоритма определяется множество существенных вхождений элементарных условий, т.е. тех, чьи значения используются для вычисления пути по графу потока управления. В графе потока управления все циклы считаются стянутыми в вершины, поэтому условия циклов и условия ветвлений внутри циклов в работе алгоритма никак не участвуют. Результат работы алгоритма — набор достижимых комбинаций элементарных условий, удовлетворяющих всем естественным или явно описанным тавтологиям. Кроме этого, составляется таблица соответствий между этими комбинациями и определяющими путями, состоящими из пройденных ветвей, а также пройденными последовательностями операторов mark. Введем следующие обозначения. Множество элементарных условий: PC.
101
102
Иначе меняем значение последнего вхождения, предшествующего последнему существенному и равное false, на true, а все следующие за ним значения — на false, делаем множество существенных вхождений пустым. Находим i [1..N]: i max(Ess) & v(i) = false. Выполняем v(i) := true j i < j N v(j) := false Ess := .
Количество вхождений элементарных условий: N. Множество индексов вхождений элементарного условия f PC: I(f) [1..N]. Текущее значение вхождения условия с индексом i [1..N]: v(i) {true, false}. Множество существенных вхождений: Ess [1..N]. Множество наборов достижимых комбинаций: Reachable. 1. Начало работы: сначала все значения условий равны false, множество существенных вхождений пусто, множество наборов достижимых комбинаций пусто. i [1..N] v(i) := false Ess := Reachable :=
Продолжаем главный цикл (идем в п. 2). Иначе, если все тавтологии выполнены, переходим в следующий пункт. 5. Вычисляем последовательно условия ветвлений в предусловии операции, используя имеющийся набор значений вхождений формул и правила вычислений логических выражений в используемом языке, и проходя в те ветви, условия которых выполнены. Каждое вхождение, значение которого понадобилось при этих вычислениях, добавляем в множество существенных вхождений.
2. Главный цикл. 3. Вложенный цикл устранения различных значений вхождений одного условия. Пока есть различные значения вхождений одного условия f PC i,j I(f) i < j & v(i) ≠ v(j). Находим пару вхождений одного и того же условия с разными значениями, в которой второе вхождение имеет наименьший индекс среди всех таких пар для всех условий и меняем в этой паре значение false на true. Находим (i, j) [1..N][1..N]: j = min{k: m < k f PC m,k I(f) & v(m) ≠ v(k)} & f PC i,j I(f) & i < j & v(i) ≠ v(j) Выполняем v(i) = false v(i) := true v(j) = false v(j) := true Возвращаемся к проверке условия вложенного цикла 3.
Если результат предусловия равен false: Если значения всех существенных вхождений равны true, конец главного цикла (идем в п. 7). Иначе, меняем значение последнего существенного вхождения, равного false, на true, а значения всех вхождений, следующих за ним, приравниваем false, и делаем множество существенных вхождений пустым. Находим i Ess : v(i) = false. Выполняем v(i) := true j i < j N v(j) := false Ess := .
Если условие вложенного цикла не выполнено — нет формул с разными значениями вхождений, переходим к следующему пункту. 4. Проверка тавтологий. Проверяем выполнение всех естественных тавтологий и тавтологий, указанных в операторах tautology, на текущем наборе значений условий.
Продолжаем главный цикл (идем в п. 2). Иначе, продолжаем работу, переходя в следующий пункт.
Если хотя бы одна тавтология нарушена, добавляем в множество существенных вхождений первые вхождения используемых в ней элементарных условий.
6. Вычисляем последовательно условия ветвлений в постусловии операции, используя имеющийся набор значений вхождений формул и правила вычислений логических выражений в используемом языке, и проходя в те ветви, условия которых выполнены. Каждое вхождение, чье значение понадобилось при этих вычислениях,
Если все значения существенных вхождений равны true, конец главного цикла (идем в п. 7). 103
104
добавляем в множество существенных вхождений. Делаем так, пока не дойдем до одного из операторов branch. Строим текущий набор значений current: для всех существенных вхождений заносим в него значения соответствующих элементарных условий, а для остальных условий (у которых нет существенных вхождений на данном проходе) заносим неопределенное значение. f PC I(f) Ess current(f) := v(i), i I(f) f PC I(f) Ess = current(f) := undef Reachable := Reachable {current}. Если значения всех существенных вхождений равны true, конец главного цикла (идем в п. 7). Иначе, меняем значение последнего существенного вхождения, равного false, на true, а значения всех вхождений, следующих за ним, приравниваем false, и делаем множество существенных вхождений пустым Находим i Ess : v(i) = false. Выполняем v(i) := true j i < j N v(j) := false Ess := . Продолжаем главный цикл (идем в п. 2). 7. Конец главного цикла. Возвращаем построенное множество Reachable. То, что приведенный алгоритм возвращает только выполнимые комбинации значений элементарных условий, следует из действий, выполняемых в п. 3 и 4. Они гарантируют, что в дальнейших пунктах используются только такие наборы значений, в которых все вхождения одного условия имеют одно значение и все тавтологии выполнены. Полноту выдаваемого набора комбинаций доказать несколько сложнее. Это доказательство опирается на то, что при наличии нескольких вхождений одного условия, их значения надо перебирать отдельно, чтобы не выбросить случайно достижимую комбинацию. Соответствующий перебор выполняется в п. 3. Кроме того, оптимизация перебора за счет выбрасывания комбинаций, отличающихся по значениям несущественных вхождений, после нарушения тавтологии выполняется иначе, чем после нарушения предусловия или полного прохода по пред- и постусловию. Это также связано с возможностью пропуска достижимых комбинаций. 105
3. Заключение В статье описаны критерии покрытия, используемые в технологии построения тестов UniTESK и основанные на структуре контрактных спецификаций функциональных требований к программному обеспечению. Несмотря на ряд особенностей спецификаций, используемых в UniTESK (операторы branch и связанные с ними требования об использовании пре- и пост-значений), эти критерии можно использовать и для произвольных контрактных спецификаций, в которых предусловия и постусловия являются вычисляемыми по некоторой схеме предикатами. Аналоги критериев покрытия функциональных ветвей и комбинаций значений элементарных условий достаточно легко переформулировать для тех случаев, когда постусловие является единой логической формулой. В таких случаях постусловие обычно представляется конъюнкцией нескольких импликаций (X1 Y1 & … & Xn Yn), посылки которых несовместны (т.е. X1, …, Xn попарно исключают друг друга). Критерий покрытия функциональных ветвей соответствует критерию покрытия посылок — эквивалентными объявляются ситуации, в которых выполнена одна из посылок Xi. Критерий покрытия комбинаций значений элементарных условий основан на представлении каждой посылки Xi в дизъюнктивной нормальной форме, при этом эквивалентными считаются ситуации, в которых выполнен один и тот же дизъюнкт в одной и той же посылке. На практике достаточно часто элементарные условия, входящие в посылки постусловия, не являются независимыми. Во-первых, они могут быть взаимосвязаны, если используют равенства и неравенства произвольных объектов, а также порядковые сравнения чисел. Во-вторых, они могут быть связаны благодаря специфическому смыслу входящих в них переменных и функций, пример чего приведен в разделе 2.3. Для определения достижимых комбинаций значений элементарных условий в инструментах UniTESK был разработан алгоритм, представленный в разделе 2.5. Он позволяет автоматически удалять из отчетов о полученном тестовом покрытии недостижимые комбинации, существенно сокращая тем самым трудоемкость анализа результатов тестирования. Известны практические примеры спецификаций, в которых количество достижимых комбинаций условий исчисляется сотнями, и примерно такое же количество комбинаций недостижимо. В этих случаях разбирать вручную, какие из непокрытых комбинаций можно покрыть, а какие — нет, крайне тяжело. Описанный алгоритм опирается на указанные пользователем тавтологии, выражающие логические зависимости между элементарными условиями. Кроме того, используются так называемые естественные тавтологии, выполненные в элементарной теории равенства и неравенства объектов (равенство является отношением эквивалентности и переносится на все атрибуты равных объектов) или в теориях порядка на целых или 106
действительных числах (с учетом машинного представления этих чисел). Для автоматического анализа естественных тавтологий используются специфические алгоритмы, описываемые в других работах. Литература [1] Guide to the Software Engineering Body of Knowledge: 2004 Edition — SWEBOK. IEEE, 2005. [2] E. W. Dijkstra. Notes on Structured Programming. Т. Н. Report 70-WSK-03. Eindhoven, Netherlands: Technological University, 1970. [3] H. Zhu, P. A. V. Hall, J. H. R. May. Software Unit Test Coverage and Adequacy. ACM Computing Surveys, 29(4):366–427, Dec. 1997. [4] I. Bourdonov, A. Kossatchev, V. Kuliamin, A. Petrenko. UniTesK Test Suite Architecture. Proceedings of FME’2002, Kopenhagen, Denmark, LNCS 2391:77–88, Springer-Verlag, 2002. [5] В. В. Кулямин, А. К. Петренко, А. С. Косачев, И. Б. Бурдонов. Подход UniTesK к разработке тестов. Программирование, 29(6):25–43, 2003. [6] А. В. Баранцев, И. Б. Бурдонов, А. В. Демаков, С. В. Зеленов, А. С. Косачев, В. В. Кулямин, В. А. Омельченко, Н. В. Пакулин, А. К. Петренко, А. В. Хорошилов. Подход UniTesK к разработке тестов: достижения и перспективы. Труды ИСП РАН, 5:121–156, 2004. [7] B. Meyer. Applying ‘Design by Contract’. IEEE Computer, 25(10):40–51, 1992. [8] B. Beizer. Software Testing Techniques. International Thomson Press, 1990. [9] A. P. Mathur. Foundations of Software Testing. Copymat Services, 2006.
107
Автоматическое определение выполнимости наборов формул для операций сравнения С. В. Зеленов, С. А. Зеленова {zelenov, sophia}@ispras.ru Аннотация. В статье излагается алгоритм автоматического определения реализуемости данного набора формул-сравнений. Алгоритм применяется в инструментах, реализующих технологию UniTESK. Его использование позволяет снизить трудозатраты на разработку спецификаций и повысить точность подсчета достигнутого тестового покрытия.
1. Введение Масштабы современного программного обеспечения (ПО) уже не позволяют обеспечить его качественную разработку без привлечения эффективных средств автоматизации создания тестов. Развиваемая в ИСП РАН технология UniTESK [1,3,5] тестирования функциональности ПО основана на использовании формальных моделей. Для автоматизации подготовки тестов и анализа результатов в UniTESK требования к ПО представляются в виде формальных спецификаций в форме пред- и постусловий (см. на Рис. 1 пример спецификации операции извлечения квадратного корня). На основе спецификаций автоматически генерируются тестовые оракулы, которые используются для проверки корректности реакции целевого ПО в ответ на единичное тестовое воздействие (вызов одной операции с некоторыми аргументами). Тесты строятся путем перебора операций и итерации наборов аргументов для каждой операции. Для оценки качества тестирования в UniTESK используются критерии покрытия спецификаций, автоматически извлекаемые из структуры последних. Базовым критерием является покрытие ветвей функциональности, каждая из которых помечается в спецификации оператором branch и соответствует тем ситуациям, в которых операция ведет себя «одинаково» (в примере на Рис. 1 выделены две ветви функциональности: когда аргумент нулевой и когда он положительный). Наиболее детальным является критерий покрытия комбинаций элементарных условий. Он определяется всеми возможными комбинациями значений 109
элементарных логических формул, использованных в условиях ветвлений спецификации данной операции. Этот критерий является аналогом критерия покрытия комбинаций условий (multiple condition coverage, см. [4], [6]) для покрытия кода. В примере на Рис. 1 имеются две элементарные формулы: f1 ≡ a >= 0, f2 ≡ a == 0. Однако при тестировании, нацеленном на достижение высокого уровня покрытия комбинаций элементарных условий, возможны проблемы, связанные с недостижимостью некоторых комбинаций в силу наличия семантических связей между элементарными формулами. А именно, если генератор отчетов не учитывает информацию о наличии недостижимых комбинаций условий, то достигнутое в результате тестирования покрытие будет им подсчитано некорректно, и в итоге в отчет о тестировании попадет заниженное значение реально достигнутого покрытия. Низкое значение достигнутого покрытия, фигурирующее в отчете, обычно служит стимулом для тестировщика к исследованию причин низкого покрытия и к попыткам его повысить, однако в случае некорректного подсчета достигнутого покрытия эта работа оказывается напрасной тратой времени. specification int sqrt( double a ) { pre { return a >= 0; } post { if( a == 0 ) { branch “zero”; return sqrt == 0; } else { branch “positive”; return a > 0 && abs( (sqrt*sqrt – x)/x ) < eps; } } }
Рис. 1. Спецификация операции извлечения квадратного корня. Так, в примере на Рис. 1 недостижимой является следующая комбинация значений элементарных формул: { f1 = false, f2 = true }. В UniTESK такие проблемы решаются при помощи описания имеющихся семантических связей в виде логических выражений-тавтологий, построенных из элементарных формул и являющихся тождественно истинными в силу смысла этих формул. Так, в примере на Рис. 1 тавтология может быть описана следующей конструкцией: tautology !( !(a >= 0) && a == 0 ); 110
Кроме подобных естественных семантических связей между элементарными формулами, которые следуют из свойств операции сравнения, бывают ситуации, когда семантические связи между элементарными формулами имеют менее явный характер. Так, в примере на Рис. 2 в первом условии проверяется, что месяц, указанный в аргументе-дате, является февралем, а во втором условии проверяется, что число месяца не меньше 30-го. Поскольку в феврале всегда не более 29 дней, здесь необходимо описать следующую тавтологию: tautology !( d.month() == FEB && d.day() >= 30 ); Как показывает практика, в реальных спецификациях достаточно большую долю тавтологий составляют именно естественные тавтологии, описывающие семантику операций сравнения. Ручное перечисление таких тавтологий является нетворческой, неинтересной, рутинной работой и может приводить к ошибкам в их описании в спецификации. В настоящей статье мы излагаем алгоритм для автоматического определения того, имеются ли при данных значениях элементарных формул нарушения естественных тавтологий, описывающих семантику операций сравнения. Алгоритм основан на анализе структуры элементарных формул и не требует явного перечисления всех таких тавтологий. Элементарные формулы Спецификация specification int m( Date d ) { post { if( d.month() == FEB ) { ... } else if( d.day() >= 30 ) { ... } else { ... } } }
f1 ≡ d.month() == FEB
a a a a a a
< > <= >= == !=
b, b, b, b, b, b,
где a и b — аргументы элементарных формул. Будем считать два аргумента одинаковыми только если совпадают их текстуальные представления. Кроме того, будем считать все аргументы переменными (т.е. будем игнорировать информацию об известных значениях константных аргументов). При перечислении комбинаций значений формул для спецификации тестируемой операции всем элементарным формулам последовательно присваиваются все возможные наборы логических значений. Каждый фиксированный набор значений элементарных формул индуцирует некоторые конкретные отношения между аргументами формул-сравнений. Для проверки истинности естественных тавтологий, описывающих семантику операций сравнения, на фиксированном наборе отношений между аргументами формул-сравнений требуется определить, является ли реализуемой такая система формул, т.е. существуют ли такие значения аргументов, при котором выполнялись бы все соответствующие отношения между ними. Нереализуемость данной системы формул означает нарушение некоторой естественной тавтологии, описывающей семантику операций сравнения, и как следствие — недостижимость соответствующей комбинации значений формул.
f2 ≡ d.day() >= 30
3. Нереализуемые ситуации
Рис. 2. Спецификация с наличием неявной семантической связи между формулами.
2. Постановка задачи
Пусть дан некоторый набор формул-сравнений. Без ограничения общности можно считать, что каждая формула имеет один из следующих видов: a > b, a >= b, a == b, a != b. Рассмотрим, какие могут встречаться нереализуемые ситуации для системы таких формул. 1. Пусть в наборе формул имеется неравенство: a != b,
Пусть в спецификации тестируемой операции имеются в том числе элементарные формулы-сравнения, т.е. формулы следующих видов: 111
и одновременно имеется поднабор формул (см. ниже), из которого следует равенство: 112
a == b. Будем называть такую ситуацию нарушением первого рода. Возможные виды формул, влекущих равенство переменных a и b суть следующие: a.
цепочка равенств, в которой участвуют a и b: a == x1 == ... == xn == b;
b.
две цепочки отношений, в которых участвуют a и b:
a [rel] y1 [rel] ... [rel] yk [rel] b, b [rel] z1 [rel] ... [rel] zm [rel] a, где каждый [rel] — это отношение из набора { >=, == }. 2.
Пусть в наборе формул имеется строгое неравенство: a > b, и пусть имеется поднабор формул, индуцирующий неравенство: b >= a,
аргументам формул. Дуги и ребра1 графа, покрашенные в разные цвета, соответствуют собственно отношениям между аргументами: отношение строгого порядка (>) представляется красной дугой от меньшего аргумента к большему; отношение нестрогого порядка (>=) представляется синей дугой от меньшего аргумента к большему; отношение неравенства (!=) представляется черным ребром; отношение равенства (==) представляется зеленым ребром. При этом приведенные выше нереализуемые ситуации представляются следующими подграфами: ситуация 1.a соответствует циклу, который содержит только несколько зеленых ребер и ровно одно черное ребро; ситуация 1.b соответствует ориентированному циклу, который содержит только синие дуги и зеленые ребра, и некоторые две вершины которого кроме того связаны черным ребром; ситуация 2 соответствует ориентированному циклу, который содержит несколько синих дуг и зеленых ребер, а также не менее одной красной дуги. Заметим, что ориентированный цикл из ситуации 1.b означает равенство всех аргументов, соответствующих его вершинам.
а именно, цепочка сравнений, в которых участвуют a и b: b [rel] x1 [rel] ... [rel] xn [rel] a,
5. Алгоритм проверки реализуемости
где каждый [rel] — это отношение из набора { >, >=, == }. Будем называть такую ситуацию нарушением второго рода. Верна следующая Теорема о реализуемости. Если данная система формул не содержит нарушений первого и второго рода, то возможна интерпретация этой системы в целых числах. Доказательство этой теоремы будет приведено ниже. Из теоремы следует, что при игнорировании информации об известных значениях константных аргументов все возможные нереализуемые ситуации исчерпываются нарушениями первого и второго рода.
Пусть дан граф, представляющий некоторую систему формул-сравнений. Алгоритм проверки реализуемости этой системы состоит из следующих этапов: 1. Отождествление (склеивание) всех пар вершин, связанных зелеными ребрами (поскольку зеленые ребра означают равенство аргументов). В результате: нереализуемая ситуация 1.a станет соответствовать черной петле; ориентированный цикл из нереализуемой ситуации 1.b станет содержать только синие дуги. 2.
4. Используемая модель Для изложения алгоритма проверки того, что данная система формулсравнений является реализуемой, мы используем модель в виде раскрашенного ориентированного графа. Вершины графа соответствуют 1
113
Поиск всех ориентированных циклов, составленных только их синих дуг, и отождествление для каждого такого цикла всех входящих в него вершин ввиду равенства соответствующих им аргументов (см. замечание в конце предыдущего раздела).
Ребра считаются неориентированными, а дуги – ориентированными. 114
В результате нереализуемая ситуация 1.b станет соответствовать черной петле. Таким образом, исходная система формул имеет нарушение первого рода тогда и только тогда, когда в образовавшемся на втором этапе графе имеется черная петля. 3.
4.
Поиск черной петли. Если черных петель в графе нет, то исходная система формул может иметь только нарушения второго рода. Ввиду отсутствия в графе ориентированных циклов, составленных только из синих дуг (они исчезли на втором этапе), любой ориентированный цикл, составленный только из дуг, автоматически будет содержать хотя бы одну красную дугу. Таким образом, исходная система формул имеет нарушение второго рода тогда и только тогда, когда в образовавшемся на третьем этапе графе имеется ориентированный цикл, составленный только из дуг. Поиск ориентированного цикла, составленного только из дуг. Если таких циклов в графе нет, то исходная система формул не имеет нарушений ни первого, ни второго рода.
Заметим, если система формул не имеет нарушений, то соответствующий ей граф, образовавшийся после работы приведенного здесь алгоритма, обладает следующими свойствами: граф составлен только из дуг и черных ребер; ни одно из черных ребер не образуют петли; подграф, образованный выбрасыванием всех черных ребер, ацикличен.
6. Доказательство теоремы о реализуемости Пусть дана система формул-сравнений, которая не содержит нарушений первого и второго рода. Рассмотрим соответствующий этой системе граф, образовавшийся после работы алгоритма, приведенного в предыдущем разделе. Будем строить интерпретацию исходной системы в целых числах путем присвоения вершинам графа некоторых целых значений так, чтобы удовлетворялись все имеющиеся в графе отношения. Поскольку все аргументы исходных формул являются переменными, мы можем назначать вершинам графа произвольные значения. Заметим, что поскольку в графе нет черных петель, т.е. все черные ребра соединяют разные вершины, то для того, чтобы удовлетворить всем имеющимся в системе отношениям неравенства (!=), которые представляются черными ребрами, достаточно присваивать всем вершинам различные значения.
Рассмотрим ациклический подграф, образованный выбрасыванием всех черных ребер. По определению дуг, использующихся в нашей модели, каждая синяя или красная дуга ведет от вершины, соответствующей меньшему аргументу, к вершине, соответствующей большему аргументу. Таким образом, для того, чтобы удовлетворить всем имеющимся в системе отношениям порядка, достаточно в качестве искомого целого значения присвоить каждой вершине ее номер в списке, полученном в результате топологической сортировки [2] данного подграфа. Теорема доказана.
7. Учет целочисленных констант Пусть в данном наборе элементарных формул для некоторых аргументов известны их константные числовые значения. Тогда если известны также типы аргументов (целочисленные или типы чисел с плавающей точкой), то даже при отсутствии нарушений первого и второго рода данная система формул может не быть реализуемой. Рассмотрим следующий пример. Пусть для целого a среди элементарных формул имеются следующие: a > 1, a < 2. Тогда комбинация значений true для обеих этих формул, недостижима. Более общей ситуацией является цепочка отношений: N [rel] x1 [rel] ... [rel] xk [rel] M, где все аргументы целые, N и M — константы, а каждый [rel] — это отношение из набора { >, >=, == }. В этом случае система формул не является реализуемой, если количество отношений строгого порядка (>) в цепочке больше, чем разность (N – M), поскольку в этом случае между N и M не найдется достаточно различных целых чисел, чтобы присвоить их в качестве значений всем аргументам xi из цепочки. Для эффективного перечисления всех таких цепочек отношений удобно использовать ациклический подграф, получающийся выбрасыванием всех черных ребер из графа, образовавшегося после окончания работы алгоритма проверки наличия нарушений первого и второго рода. Рассмотрение ситуаций, когда в цепочке какой-то из аргументов не является целым, существенно усложняется в связи с дискретностью представления на ЭВМ чисел с плавающей точкой и непредставимостью в этом типе некоторых (в том числе целых) чисел. В частности, существуют такие последовательные числа2 с плавающей точкой X и Y, что между ними есть число, представимое 2
115
Т.е. такие, между которыми не существует других чисел того же типа. 116
как целое, но не как число с плавающей точкой, например, 3107 представимо в виде числа с плавающей точкой однократной точности, а следующим таким представимым числом является 3107 + 2. Поэтому система формул 3107 < a и a < 3107 + 2 невыполнима, если a имеет тип числа с плавающей точкой однократной точности и выполнима, если a — 32-битное целое. Анализ таких ситуаций мы оставляем за рамками настоящей статьи.
8. Заключение В работе предложен алгоритм поиска в системе элементарных формул нарушений естественных тавтологий, описывающих семантику операций сравнения. Алгоритм основан на анализе структуры элементарных формул и не требует явного перечисления всех таких тавтологий. Алгоритм успешно используется в инструментах, реализующих технологию тестирования UniTESK [1,3,5]. Он позволяет повысить точность автоматического измерения достигнутого тестового покрытия, а также дает возможность тестировщику не писать многочисленные тавтологии, описывающие взаимосвязи между имеющимися в спецификации формулами сравнения, что позволяет существенно сократить ручные трудозатраты и количество ошибок при написании спецификаций целевого ПО. Литература [1] А. В. Баранцев, И. Б. Бурдонов, А. В. Демаков, С. В. Зеленов, А. С. Косачев, В. В. Кулямин, В. А. Омельченко, Н. В. Пакулин, А. К. Петренко, А. В. Хорошилов. Подход UniTesK к разработке тестов: достижения и перспективы. Труды ИСП РАН, 5:121–156, Москва, 2004. [2] Т. Кормен, Ч. Лейзерсон, Р. Ривест. Алгоритмы: построение и анализ. М.: МЦНМО, 1999. [3] В. В. Кулямин, А. К. Петренко, А. С. Косачев, И. Б. Бурдонов. Подход UniTesK к разработке тестов. Программирование, 29(6):25–43, 2003. [4] A. P. Mathur. Foundations of Software Testing. Copymat Services, 2006. [5] UniTESK. http://www.unitesk.ru/ [6] H. Zhu, P. A. V. Hall, J. H. R. May. Software Unit Test Coverage and Adequacy. ACM Computing Surveys, 29(4):366–427, Dec. 1997.
117
Генератор сложных данных Pinery: реализация новых возможностей UniTESK А. В. Демаков, С. В. Зеленов, С. А. Зеленова {demakov, zelenov, sophia}@ispras.ru Аннотация. В статье рассказывается о подходе к автоматической генерации тестовых данных сложной структуры, основанном на использовании формального описания данных в виде грамматик. Подход реализован в генераторе Pinery, который является базой для создания переиспользуемых библиотек абстрактных моделей и итераторов для генерации специфических данных. В работе описывается метод разработки подобных расширений, позволяющий в результате получать небольшие множества тестовых данных, нацеленных на тестирование заданных аспектов функциональности программного обеспечения.
1. Введение Тестирование программного обеспечения (ПО) с целью контроля его качества является одним из важнейших этапов разработки ПО. При тестировании ПО, обрабатывающего данные сложной структуры — к такого рода данным относятся, например, другие программы, сообщения телекоммуникационных протоколов, XML-документы, содержимое баз данных и пр. — построение представительного множества тестов становится чрезвычайно трудоемкой задачей. Наиболее привлекательным здесь является использование тех или иных автоматических генераторов тестовых данных. Разработанная в ИСП РАН технология автоматизированного тестирования UniTESK [2,7,30] нацелена на тестирование функциональности целевого ПО и основана на использовании формальных моделей ПО. Тестовые воздействия в UniTESK строятся путем перебора вызовов интерфейсных операций целевого ПО и итерации наборов аргументов для каждой операции. Инструменты, поддерживающие UniTESK, предоставляют разработчикам тестов библиотеки базовых итераторов значений простых типов, которые могут быть непосредственно использованы для генерации тестовых воздействий, а могут быть скомпонованы в более сложные генераторы. Технология UniTESK оказалась удобной для тестирования в том числе и программных систем, обрабатывающих данные сложной структуры: 119
реализаций телекоммуникационных протоколов [6], моделей аппаратного обеспечения [5]. Однако, для итерации аргументов операций в этих проектах библиотечные итераторы UniTESK оказались недостаточными, и использовались более подходящие сторонние генераторы. При этом каждый раз приходилось решать рутинные технические задачи по адаптации генераторов тестовых данных к используемым инструментам UniTESK. В настоящей статье мы описываем унифицированный подход к итерации тестовых данных сложной структуры в UniTESK. Подход основан на полученном в ИСП РАН опыте разработки тестовых данных для различных областей ПО и опыте разработки автоматических генераторов тестовых данных на основе грамматик и языковых моделей [1,4,8,20,22,33,34]. Подход реализован в генераторе тестовых данных Pinery, который может использоваться как в качестве итератора в инструментах UniTESK, так и в виде самостоятельного инструмента генерации.
2. Близкие работы В настоящее время имеется ряд инструментов для автоматической генерации разного рода тестовых данных сложной структуры: для тестирования приложений над базами данных [10,19], для тестирования обработчиков XMLдокументов [31,32] и др. Существенным недостатком этих инструментов является то, что в них предоставляется слишком бедный набор возможностей для настройки процесса генерации. В результате для достижения приемлемого качества тестирования, приходится генерировать огромные множества тестовых данных, что ведет к чрезмерным затратам ресурсов как на этапе генерации, так и на этапе запуска тестов. В подходах, представленных в работах [11,14,28], используются грамматики в виде расширенной BNF, снабженные специальными фрагментами программного кода, которые содержат информацию семантического характера: разного рода вспомогательные вычисления и проверки условий. Такое представление семантики в виде произвольного программного кода, разбросанного во многих разных местах описания, приводит к высокой трудоемкости сопровождения входных данных генератора. В статье [24] описан основанный на грамматиках генератор DGL, который в первую очередь предназначен для генерации тестов на случайной основе. Генерация регулируется путем присвоения продукциям некоторых значений весов. Кроме того, имеется возможность вводить вспомогательные вычисления, а также устраивать систематическую итерацию атрибутов и альтернатив. Все перечисленные выше подходы нацелены на то, чтобы сгенерировать какие-нибудь тестовые данные, удовлетворяющие предоставленному описанию. Однако в них не рассматриваются какие бы то ни было критерии полноты получаемых наборов тестовых данных. 120
В статьях [16,17,18] описываются подходы к автоматической генерации тестов для семантических анализаторов, основанные на спецификации семантики в виде атрибутных грамматик [26]. Другой подход [20,21] к этой задаче использует спецификации в виде ASM [15]. Авторы этих подходов рассматривают различные критерии тестового покрытия для семантических анализаторов и описывают соответствующие автоматические генераторы тестов. В общих чертах эти генераторы работают так: сначала генератор создает множество синтаксически корректных предложений, которые затем проверяются на семантическую корректность с помощью интерпретатора атрибутной грамматики или ASM, и все семантически некорректные предложения отбрасываются. Такие генераторы оказываются чрезвычайно ресурсоемкими: как показывает практика, для генерации 3 000 семантически корректных тестов требуется предварительно сгенерировать порядка 3 000 000 синтаксически корректных тестов. В работе [9] описан генератор тестов Korat, который использует спецификацию тестовых данных в виде Java-метода, проверяющего корректность структуры данных. Кроме того, генератору требуется предоставить набор параметров, отвечающих за то, чтобы множество генерируемых данных было конечным. В статье [23] описан основанный на грамматиках генератор тестов Geno. Этот генератор имеет ряд простых механизмов настройки генерации, таких как ограничение глубины дерева, ограничение глубины рекурсии, задание комбинатора и т.п. Общим недостатком всех перечисленных выше подходов является то, что в них отсутствует возможность тонкой настройки генератора для построения тестов, нацеленных на тестирование некоторого заданного аспекта функциональности целевого ПО. Имеющиеся возможности позволяют лишь тем или иным образом уменьшить количество генерируемых тестов и избежать комбинаторного взрыва. В работе [11] представлен метод для автоматического тестирования ПО, осуществляющего рефакторинг программного кода. Для генерации тестов, нацеленных на тестирование определенного вида рефакторинга, требуется разработать соответствующий генератор с использованием библиотеки ASTGen, созданной авторами метода. В статьях [22,34] описан метод OTK для тестирования оптимизирующих компиляторов на основе моделей. Метод описывает способ построения модели по абстрактному описанию алгоритма оптимизации, выраженного на естественном языке. Для генерации тестов на основе построенной модели требуется разработать определенные компоненты генератора. Метод OTK снабжен одноименным инструментом, который предоставляет поддержку для формального описания модели данных, а также для разработки всех требующихся компонентов генератора тестов. 121
Главным достоинством этих двух подходов является следующее: поскольку генераторы тестовых данных фактически разрабатываются вручную, их довольно легко нацелить на генерацию данных для тестирования требуемого аспекта функциональности целевого ПО (например, определенного вида рефакторинга или определенного оптимизатора). Однако платой за это является высокая трудоемкость сопровождения каждого конкретного генератора: часто для модернизации имеющегося генератора с тем, чтобы генерируемые данные обладали некоторым дополнительным свойством, требуется заново разработать порядка половины кода генератора. Итак, основными недостатками существующих в настоящее время подходов и генераторов тестовых данных являются следующие (и/или): бедный набор возможностей для настройки процесса генерации; высокая трудоемкость сопровождения входных данных генератора; использование массовой генерации тестов-кандидатов с последующей фильтрацией неподходящих кандидатов, что является причиной чрезмерных затрат ресурсов; плохая нацеливаемость на тестирование некоторого заданного аспекта функциональности целевого ПО; отсутствие критериев оценки качества совокупности генерируемых данных.
3. Модельное представление тестовых данных Будем исходить из того, что данные сложной структуры составлены из частей, которые организованы в некоторую иерархию, например: программа – функция – инструкция – аргумент; документ – раздел – параграф – предложение. При этом некоторые части иерархии могут быть как-то связаны между собой. Например, используемый в программе на языке со строгой типизацией идентификатор должен быть где-то объявлен. Такие связи мы будем называть горизонтальными. Описание возможных иерархических структур часто задается в виде грамматики: схемы XML-документов описываются на языках DTD или XMLSchema, структуры реляционных баз данных часто задаются в виде скрипта создания базы данных на языке SQL, синтаксис формальных языков (например, языков программирования) традиционно описывается в форме Бэкуса-Наура (BNF). Поскольку в грамматиках в основном определяются лишь связи вида «предок–потомок»1, для задания горизонтальных связей как правило дополнительно к грамматике описывается соответствующий набор ограничений (см. ниже). 1
Такие языки, как XMLSchema и SQL позволяют задавать также горизонтальные связи некоторых видов. 122
В предлагаемом здесь подходе модель конкретной иерархической структуры представляется в виде атрибутированного гетерогенного дерева. Пример. Рассмотрим следующую BNF-грамматику арифметических выражений (для простоты, в этих выражениях используется только сложение чисел и скобки). E ::= F ( “+” F )* ; F ::=
| P ; P ::= “(” E “)” ; В качестве атрибутированного дерева, соответствующего некоторому выражению, мы рассматриваем его дерево абстрактного синтаксиса. Для данной грамматики оно состоит из вершин следующих типов. Тип E, вершины которого имеют дочерние вершины: o ребенок типа F, соответствующий «головному» слагаемому; o список из нуля или более детей типа F, соответствующий «хвостовым» слагаемым. Тип F, у которого имеется два подтипа: o тип FN , вершины которого содержат атрибут, представляющий число; o тип FP , вершины которого имеют ребенка типа P, соответствующего подвыражению, взятому в скобки. Тип P, вершины которого имеют ребенка типа E, соответствующего подвыражению, которое содержится внутри скобок. ►
Рис. 1. Архитектура генератора Pinery
4. Архитектура генератора Pinery Генератор Pinery состоит из следующих основных компонентов (Рис. 1): итератор атрибутированных деревьев; анализатор достигнутого покрытия; транслятор для отображения моделей в тесты. 123
В общих чертах генерация тестов происходит так (схематичная иллюстрация процесса генерации показана на Рис. 11, псевдокод алгоритма генерации приведен на Рис. 2). Итератор атрибутированных деревьев генерирует деревья последовательно по одному. Если очередное сгенерированное дерево увеличивает уровень достигнутого покрытия, то дерево отображается в тестовые данные, которые могут использоваться для тестирования целевой системы. При этом собственно тестовые данные могут представлять собой как текст на некотором формальном языке (например, тексты программ для тестирования компилятора), так и сложную структуру объектов в памяти ЭВМ (например, для модульного тестирования программных интерфейсов систем, работающих с такого рода объектами). void run( Config c ) { Iterator it = createIterator( c ); for( it.init(); it.has(); it.next() ) { if( isCoverageIncreased( it.value() ) ) { mapToTest( it.value() ); } } } Рис. 2. Псевдокод алгоритма генерации Pinery Процесс итерации управляется набором ограничений, природа которых бывает двух видов: необходимые ограничения, выражающие семантику генерируемых данных (например, ограничения, описывающие горизонтальные связи); дополнительные ограничения, служащие для снижения количества генерируемых тестов до приемлемого уровня (например, ограничения на длины генерируемых списков, на глубину генерируемых деревьев и пр.). Каждое ограничение представляет собой предикат, зависящий от структуры некоторого поддерева. Ограничение работает в итераторе как фильтр, который отсеивает все те построенные поддеревья, которые не удовлетворяют данному ограничению, т.е. на которых соответствующий предикат не выполняется. Вся информация о наложенных ограничениях, о выбранном критерии покрытия, о способе отображения моделей в тесты и о всех прочих настройках передаются в генератор через специальный конфигурационный файл. Подробнее алгоритм итерации атрибутированных деревьев Pinery изложен в работе авторов [3]. Далее мы рассмотрим некоторые общие вопросы предлагаемого подхода. 124
5. Цель генерации Пусть на множестве всех входных данных тестируемой программной системы задан некоторый критерий тестового покрытия. Тогда целью генерации атрибутированных деревьев является получение такого набора тестовых данных, на котором достигается заданный уровень покрытия по этому критерию. В тех случаях, когда уровень достигнутого тестового покрытия удается вычислять непосредственно в процессе генерации данных, генератор можно снабдить соответствующим анализатором покрытия и останавливать процесс генерации при достижении заданного уровня покрытия. Пример. В работе [27] Пардом предложил следующий способ задания элементов покрытия синтаксически корректных предложений для тестирования парсеров: в один элемент покрытия входят те предложения, в дереве вывода которых использована определенная продукция грамматики данного языка. Таким образом, некоторая данная грамматика определяет столько элементов покрытия, сколько в ней содержится продукций. Для грамматики аддитивных выражений (см. выше) этот способ определяет три элемента покрытия: для продукций E, F и P. ►
6. Настройка эффективной итерации деревьев Использование ограничений как предикатов для выбора конечного подмножества итерируемых деревьев не является эффективным, поскольку использование фильтра не влияет на скорость генерации всего фильтруемого поддерева. В Pinery имеются следующие базовые средства настройки эффективной итерации деревьев: задание специфического итератора; задание комбинатора; использование контекста генерации дерева.
использовать это средство без чрезмерных трудозатрат удается в основном лишь для значений скалярных атрибутов. Однако, даже такое узкое использование специфических итераторов дает существенный выигрыш во времени генерации и в качестве получаемых результатов по сравнению с использованием итераторов по умолчанию.
Комбинатор Другим средством настройки эффективной итерации является задание комбинатора итерируемых значений полей некоторого узла. По умолчанию в качестве комбинатора используется итератор декартова произведения, перебирающий все возможные комбинации значений полей. Простейшим комбинатором, позволяющим существенно снизить количество перебираемых комбинаций, является так называемый итератор диагонали, перебирающий такое множество S кортежей, что i {1, , n} s S i ( s1 , , s n ) S : s = si, где Si – это множество значений, итерируемое i-м подчиненным итератором. Выбор того или иного комбинатора в каждом конкретном случае обычно бывает продиктован структурой выбранного покрытия.
Контекст генерации дерева Еще одним средством настройки эффективной итерации является использование контекста, т.е. информации об уже построенной части дерева. Рассмотрим, например, следующее семантическое ограничение: имя используемой переменной в программе может использоваться только после объявления этой переменной. Можно так настроить итератор Pinery, чтобы в процессе итерации для имени используемой переменной перебирались только имена переменных, которые имеются в контексте. Другим примером является настройка эффективной итерации деревьев, удовлетворяющих заданному ограничению на глубину получающихся деревьев. В этом случае можно использовать имеющуюся в контексте информацию о текущей глубине вновь создаваемого узла дерева. Аналогично можно организовать ограничение глубины рекурсии относительно вершин данного типа.
Специфический итератор По умолчанию для перебора значений данного поддерева в Pinery создается некоторый стандартный итератор. Если на данное поддерево наложено какоето ограничение, то соответствующий ему предикат будет применяться для фильтрации значений именно этого стандартного итератора. Простейшим средством настройки эффективной итерации данного поддерева является задание для использования в качестве итератора его значений некоторого специфического итератора, который будет заведомо выдавать поддеревья, удовлетворяющие данному ограничению. На практике 125
7. Эффективность генератора Как было сказано выше, основной целью генерации тестовых данных является достижение заданного критерия покрытия. При этом из практических соображений требуется, чтобы генератор работал некоторое достаточно недолгое время. Время работы генератора, а также уровень покрытия, достигаемый на построенных тестах, очень сильно зависят от того, какие были заданы настройки итерации. 126
Пример. Рассмотрим пример с аддитивными выражениями. Предположим, что в качестве цели генерации используется покрытие продукционных правил в дереве вывода2 с критерием покрытия 100%. Пусть для итерации используются следующие настройки: Ilen: итератор значений длины списка «хвостовых» слагаемых выражения; IN: итератор значений атрибута-числа ; R: глубина рекурсии относительно вершин типа E. Пусть при переборе значений слагаемых сперва используются генераторы G(FN) вершин типа FN с комбинатором декартова произведения, и лишь после этого используются генераторы G(FP) вершин типа FP. При значениях настроек Ilen = {0}, IN = {2}, R = 1, будет построено единственное дерево, соответствующее выражению «2». При этом будет достигнуто покрытие 67%: покрыто два правило из трех (E и F). При значениях настроек Ilen = {0, 1}, IN = {2, 3}, R = 2, будет построено семь деревьев, соответствующих выражениям «2», «3», «2+2», «2+3», «3+2», «3+3», «(2)». Из этих семи деревьев анализатор покрытия пропустит только два: «2» и «(2)». Остальные деревья будут отброшены, поскольку не увеличивают покрытия: на первом же дереве покрыты два правила из трех (E и F), а третье правило (P) покрыто лишь на седьмом дереве. Итак, достигнуто покрытие 100%, однако генератор работал не эффективно, поскольку из семи сгенерированных деревьев пять было отброшено анализатором покрытия. При значениях настроек Ilen = {0}, IN = {2}, R = 2, будет построено два дерева, соответствующих выражениям «2», «(2)». Оба дерева будут пропущены анализатором покрытия, и при этом будет достигнуто покрытие 100%. ► Очевидно, что эффективнее всего генератор будет работать тогда, когда ни одно из сгенерированных деревьев не будет отброшено анализатором покрытия. Следует, однако, учитывать, что для реальной задачи, скорее всего, будет очень нелегко подобрать такие точные настройки, при которых заданный критерий покрытия будет достигаться совсем без фильтрации. Вероятнее всего, будет дешевле задать некоторые более грубые настройки генерации, при которых какая-то приемлемая часть сгенерированных деревьев все-таки будет отброшена, но зато заданный критерий покрытия будет
2
Выбор такой цели генерации здесь является искусственным и направлен на то, чтобы в простой ситуации проиллюстрировать зависимость эффективности генератора от настроек итерации. В реальности же более адекватной целью генерации для данного примера является покрытие различных длин списка «хвостовых» слагаемых и различных достигнутых глубин рекурсии. 127
достигнут при затрате приемлемого времени работы тестировщика на настройку генератора.
8. Задачи тестировщика Итак, основной задачей тестировщика при использовании генератора Pinery является следующее. 1. Определить адекватную цель генерации и соответствующий ей анализатор покрытия. 2.
Задать такие настройки генерации, чтобы, a. потратив на это приемлемое время, b. получить покрытие 100%, c. при приемлемой доле отброшенных при фильтрации данных.
Как показывает практика, для результативной генерации тестовых данных, нацеленных на тестирование данного ПО, описанных выше базовых средств оказывается недостаточно. Эти средства в основном позволяют лишь в некоторой степени уменьшить количество генерируемых данных. Однако, с их помощью очень трудно настроить генератор на построение данных, имеющих некоторую специфическую структуру (см. пример ниже). Кроме того, огромное количество настроек для объемных моделей вызывает большие трудности при сопровождении конфигурации генератора. В предлагаемом здесь подходе для решения этой проблемы используются модели разного уровня абстракции, которые позволяют моделировать различные аспекты генерируемых данных в виде отдельных абстрактных моделей, выраженных в некоторых подходящих математических терминах. Экземпляры абстрактных моделей генерируются независимо и затем отображаются в общую конкретную модель тестовых данных. В результате это позволяет генерировать небольшие множества тестовых данных, нацеленных на особенности тестируемого ПО. Таким образом, для получения нацеленных тестовых данных, тестировщик должен предварительно решить следующие задачи: выделить целевые аспекты тестовых данных и выразить их в виде подходящих абстрактных моделей; описать отображение абстрактных моделей в общую конкретную модель. При использовании абстрактных моделей конфигурация генератора получается намного проще, понятнее и управляемее ввиду фактического разделения ее на модули, соответствующие разным абстрактных моделям.
128
9. Пример абстрактной модели
10. Использование абстрактных моделей в Pinery
Предположим, что мы генерируем тесты для компилятора языка C для тестирования в нем оптимизатора, осуществляющего удаление недостижимого кода (Unreachable-Code Elimination, см. [25]). Алгоритм этой оптимизации в основном работает со структурой линейных участков программы и переходов между ними. Пусть имеется некоторая модель программ на языке C, в которой линейные участки моделируются помеченными блоками, каждый из которых может завершаться переходом goto: label_A: { ... // инструкции goto label_B; } Будем генерировать каждый тест в виде функции, состоящей из последовательности помеченных блоков. При этом каждый тест должен успешно компилироваться и выполняться. Чтобы гарантировать завершение выполнения теста за конечное время, можно генерировать функции с ациклической структурой переходов goto (например, чтобы каждый переход goto вел на метку одного из последующих блоков)3. Для моделирования структуры переходов будем использовать абстрактную модель, выраженную в виде ациклического графа. Для построения одного теста при этом потребуется отобразить структуру очередного построенного ациклического графа в структуру переходов goto. Различные структуры ациклических графов при этом порождают тесты с различной структурой переходов goto. Таким образом, использование такой абстрактной модели позволяет получать тесты со следующими свойствами: a. все тесты различны и при этом «интересны» в плане их нацеленности на тестируемую функциональность; b. каждый тест при выполнении гарантированно не зацикливается.
Как уже было сказано, сложные структуры данных мы моделируем в виде атрибутированных деревьев. При этом горизонтальные связи (см. на Рис. 3.a пример связи, описывающей переход на метку) моделируются путем использования пары атрибутов, один из которых представляет идентификатор, а другой — ссылку на этот идентификатор (см. соответствующие атрибуты label и goto на Рис. 3.b). Связь считается установленной, если значение атрибута-ссылки равно значению атрибута-идентификатора.
Кроме того, количество неизоморфных ациклических графов сравнительно невелико. Итак, в результате мы получим небольшое множество тестов, нацеленных на тестирование данного оптимизатора. Получить множество тестов с похожими свойствами, используя лишь базовые средства настройки генератора, было бы гораздо сложнее.
3
Если для тестирования требуется наличие в функции циклической структуры, следует объявить цикл отдельным элементом модели и генерировать циклы, которые выполняются конечное количество раз. 129
Рис. 3. Моделирование горизонтальной связи путем использования пары атрибутов «идентификатор» (label) и «ссылка» (goto). В примере, обсуждавшемся в предыдущем разделе, ациклический граф моделирует структуру переходов goto в модели программ на языке C. Таким образом, исходную модель программ на C можно назвать конкретной моделью, а ациклический граф – абстрактной моделью. Для данного модельного дерева M (см. на Рис. 4.a модельное дерево для обсуждавшегося выше примера) можно построить его абстракцию (Рис. 4.c), путем игнорирования некоторых его «несущественных» узлов (Рис. 4.b). После этого полученную абстракцию можно представить в виде соответствующей абстрактной модели N, выраженной в подходящих математических терминах (например, ациклический граф на Рис. 4.d). Процедура построения абстракции для моделей, описанных в виде грамматик, подробно обсуждается в работе [4]. Пусть в рассмотренном выше примере соответствующая часть BNF грамматики для исходной модели M выглядит так4:
4
Здесь константные терминалы опущены, атрибуты представлены в угловых скобках, фигурные скобки могут использоваться для именования подвыражений BNF. 130
: -> :,
Рис. 4. Построение абстрактной модели N для модельного дерева M. a. Исходное модельное дерево M; b. Игнорирование несущественных узлов; c. Абстракция дерева M; d. Соответствующая абстрактная модель N. Func ::= Head Body; Head ::= ... Body ::= {blocks:Block*}; Block ::= Insn* ?; Insn ::= ... ... Тогда соответствующая BNF грамматика для абстрактной модели N такова: DAG ::= {vertices:Vertex*}; Vertex ::= ?; При построении теста сначала создается экземпляр абстрактной модели, а затем в соответствии с ней создается экземпляр конкретной модели. Для определения отображения абстрактной модели в конкретную модель требуется описать соответствующие правила в терминах грамматик для абстрактной и конкретной модели. Набор правил для приведенных здесь грамматик выглядит так: DAG:vertices -> Body:blocks; Vertex:id -> Block:label; Vertex:ref -> Block:goto; Каждое правило имеет вид 131
где – имя элемента-прообраза в контексте абстрактной модели, а – имя соответствующего элемента-образа в контексте конкретной модели. В ходе построения экземпляра конкретной модели по данному экземпляру абстрактной модели, генератор Pinery ищет потенциальные пары «элементпрообраз—элемент-образ», которые удовлетворяют некоторому правилу. Если найденный потенциальный элемент-образ является атрибутом, то в качестве его значения устанавливается значение соответствующего найденного элемента-прообраза. Иначе поиск пар продолжается в контексте найденных поддеревьев. Значения тех элементов конкретной модели, для которых не нашлось элемента-образа в абстрактной модели, достраиваются соответствующими стандартными или заданными в конфигурации специфическими итераторами. Отображение определено корректно, если в данном наборе правил каждый атрибут-образ встречается не более одного раза.
11. Использование нескольких абстрактных моделей Для данной модели M абстрактная модель N отражает некоторый аспект из M. В ряде случаев бывает целесообразно отразить несколько аспектов из M в нескольких отдельных абстрактных моделях N1, …, Nk. Пример. В представленном выше примере используется одна абстрактная модель, которая отражает структуру переходов goto в одной функции. Предположим теперь, что мы хотим генерировать каждый тест в виде нескольких функций, которые вызывают друг друга. Чтобы гарантировать выполнение теста за конечное время, мы можем генерировать ациклическую структуру вызовов функций. Таким образом, здесь будет использовано две абстрактные модели: одна из них отражает структуру переходов goto в одной функции, а другая – структуру вызовов функций. ► Для данной модели M использование нескольких абстрактных моделей N1, …, Nk корректно, если наборы правил отображения в модель M из различных абстрактных моделей описывают попарно непересекающиеся множества атрибутов-образов. Кроме использования нескольких абстрактных моделей для одной конкретной модели, иногда бывает целесообразно рассмотреть уже используемую абстрактную модель в качестве конкретной и строить ее абстракции 132
аналогичным образом. Таким образом может получиться многоуровневая иерархия абстрактных моделей.
12. Библиотеки абстрактных моделей и итераторов Использование простых абстрактных моделей, выраженных в математических терминах, дает возможность эффективно итерировать экземпляры абстрактных моделей с помощью широко известных методов перечислительной комбинаторики (см. например [13,29]). Если абстрактная модель N отражает некоторый аспект конкретной модели, то можно легко изменять свойства тестов, касающиеся этого аспекта, путем смены итератора экземпляром модели N. Так, если в обсуждавшемся выше примере потребуется генерировать некоторую другую структуру переходов goto, то для этого нужно будет использовать соответствующий итератор абстрактных графов. Замечательно, что такая модификация не потребует изменения других частей конфигурации генератора, в том числе описаний используемых моделей и правил отображений абстрактных моделей в конкретную модель. Таким образом, можно независимо управлять свойствами, относящимися к различным аспектам генерируемых тестов, если эти аспекты отражены в разных абстрактных моделях. Кроме того, абстрактные модели удается легко переиспользовать в различных проектах. Чтобы использовать абстрактную модель N для генерации экземпляров данной конкретной модели M, требуется лишь описать соответствующие правила отображения из N в M. Для создания абстрактной модели для генератора Pinery требуется: описать синтаксическую структуру абстрактной модели; создать конфигурацию генератора, которая позволит строить экземпляры абстрактной модели, обладающие заданными свойствами; разработать на языке Java специфические итераторы, которые используются в конфигурации генератора.
13. Заключение В работе представлен подход к автоматической генерации тестовых данных сложной структуры, использующий формальные описания тестовых данных в виде грамматик и встраиваемый в технологию автоматизированного тестирования UniTESK, которая основана на формальных спецификациях и моделях и поддерживает весь цикл тестирования начиная от определения требований к целевой системе и заканчивая анализом результатов тестирования. Для практического использования предложенного подхода разработан генератор тестовых данных Pinery, который: 133
позволяет генерировать данные в соответствии с заданными критериями тестового покрытия; предоставляет достаточный для большинства практических нужд набор средств для настройки процесса генерации; обеспечивает генерацию тестовых данных как в виде текста на некотором формальном языке, так и в виде сложной структуры объектов в памяти ЭВМ.
Генератор Pinery можно рассматривать как базовую среду, которая позволяет создавать специализированные расширения путем разработки библиотек абстрактных моделей и итераторов для решения разнообразных задач генерации специфических данных. В работе описывается метод для разработки подобных специализированных генераторов, который: позволяет генерировать сравнительно не большие множества тестовых данных, нацеленных на тестирование заданных аспектов функциональности ПО; обеспечивает модульность конфигурации генератора и переиспользуемость отдельных модулей. Как показала практика, наибольший эффект при разработке тестовых данных в рамках представленного подхода достигается в случае активного использования абстрактных моделей данных. Литература [1] М.В. Архипова. Генерация тестов для семантических анализаторов. Вычислительные методы и программирование, том 7, раздел 2, 55–70, 2006. [2] А.В. Баранцев, И.Б. Бурдонов, А.В. Демаков, С.В. Зеленов, А.С. Косачев, В.В. Кулямин, В.А. Омельченко, Н.В. Пакулин, А.К. Петренко, А.В. Хорошилов. Подход UniTesK к разработке тестов: достижения и перспективы. Труды ИСП РАН, 5:121–156, Москва, 2004. [3] А.В. Демаков, С.В. Зеленов, С.А. Зеленова. Генерация тестовых данных сложной структуры с учетом контекстных ограничений.Труды ИСП РАН, Москва, 2006, т. 9, 83–96. [4] С.В. Зеленов, С.А. Зеленова, А.С. Косачев, А.К. Петренко. Генерация тестов для компиляторов и других текстовых процессоров. Программирование, 29(2):59–69, 2003. [5] В.П. Иванников, А.С. Камкин, В.В. Кулямин, А.К. Петренко. Применение технологии UniTesK для функционального тестирования моделей аппаратного обеспечения. Препринт 8 ИСП РАН, Москва, 2005. [6] Г.В. Ключников, А.К. Косачев, Н.В. Пакулин, А.К. Петренко, В.З. Шнитман. Применение формальных методов для тестирования реализации IPv6. Труды ИСП РАН, 4:121–140, 2003, Москва. [7] В.В. Кулямин, А.К. Петренко, А.С. Косачев, И.Б. Бурдонов. Подход UniTesK к разработке тестов. Программирование, 29(6):25–43, 2003.
134
[8] А.К. Петренко и др. Тестирование компиляторов на основе формальной модели языка. Препринт института прикладной математики им. М.В Келдыша, № 45, 1992. [9] C. Boyapati, S. Khurshid, and D. Marinov. Korat: Automated Testing Based on Java Predicates. Proc. of ISSTA 2002, Rome, Italy. July 2002. [10] Canam Software Turbodata. http://www.turbodata.ca/. [11] B. Daniel, D. Dig, K. Garcia, D. Marinov. Automated Testing of Refactoring Engines. ESEC/FSE’07, 185–194, 2007. [12] A. Duncan, J. Hutchison. Using attributed grammars to test designs and implementation. In Proceedings of the 5th international conference on Software engineering, 170–178, 1981. [13] I.P. Goulden, D.M. Jackson. Combinatorial Enumeration. Wiley, 1983. [14] R.F. Guilmette. TGGS: A flexible system for generating efficient test case generators, 1995. [15] Y. Gurevich. Abstract state machines: An overview of the project. LNCS, 2942, 6–13, 2004. [16] J. Harm. Automatic test program generation from formal language specifications. Rostocker Informatik-Berishte, 20, 33–56, 1997. [17] J. Harm, R. Lämmel. Testing attribute grammars. In Proceedings of Third Workshop on Attrite Grammars and their Applications, 79–98, 2000. [18] J. Harm, R. Lämmel. Two-dimensional approximation coverage. Informatica, 24(3), 2000. [19] IBM DB2 Database Test Generator. http://www-306.ibm.com/software/data/db2imstools/db2tools/db2tdbg/ [20] A. Kalinov, A. Kossatchev, A. Petrenko, M. Posypkin, V. Shishkov. Using ASM specifications for compiler testing. LNCS 2589:415, 2003. [21] A. Kalinov, A. Kossatchev, M. Posypkin, V. Shishkov. Using ASM specification for automatic test suite generation for mpC parallel programming language compiler. In Proceedings of Fourth International Workshop on Action Semantic, AS'2002, BRICS note series NS-02-8, 99–109, 2002. [22] A. Kossatchev, A. Petrenko, S. Zelenov, S. Zelenova. Application of model-based approach for automated testing of optimizing compilers. In Proceedings of the International Workshop on Program Understanding, Novosibirsk, 81–88, 2003. [23] R. Lämmel, W. Schulte. Controllable combinatorial coverage in grammar-based testing. In TestCom, LNCS 3964:19–38, 2006. [24] P.M. Maurer. Generating test data with enhanced context-free grammars. IEEE Software, 7(4):50–55, 1990. [25] S. Muchnick. Advanced Compiler Design and Implementation. Morgan Kaufmann Publishers, 1997. [26] J. Paakki. Attribute grammar paradigms – a high-level methodology in language implementation. ACM Computing Surveys, 27(2):196–255, 1995. [27] P. Purdom. A sentence generator for testing parsers. Behavior and Information Technology, 12(3):366–375, 1972. [28] E.G. Sirer, B.N. Bershad. Using production grammars in software testing. In Second Conference on Domain-Specific Languages, 1–13, 1999. [29] R.P. Stanley. Enumerative Combinatorics, 2 Vols. Cambridge University Press, 1996/1999. [30] UniTESK. http://www.unitesk.ru/. [31] XML Generator. http://www.stylusstudio.com/xml_generator.html.
135
[32] XML-XIG. http://sourceforge.net/projects/xml-xig. [33] S. Zelenov, S. Zelenova. Automated generation of positive and negative tests for parsers. LNCS 3997:187–202, 2006. [34] S. Zelenov, S. Zelenova. Model-based testing of optimizing compilers. In Proc. of the 19th IFIP TC6/WG6.1 International Conference on Testing of Software and Communicating Systems — 7th International Workshop on Formal Approaches to Testing of Software (TestCom/FATES), LNCS 4581:365–377, 2007.
136
методу локализации ошибок путём автоматического минимальных условий, необходимых для их воспроизведения.
Локализация ошибок методом сокращенного воспроизведения трассы С. Г. Грошев [email protected] Аннотация. В статье предложен метод построения на основе существующего теста UniTESK, находящего ошибку в тестируемой системе, минимального теста, обнаруживающего ту же ошибку. Полученный минимальный тест может использоваться для локализации ошибки в реализации тестируемой системы. Приведено математическое обоснование предложенного метода. Предложен алгоритм, реализующий его, и доказана корректность алгоритма. Описана реализация предложенного метода для инструментария тестирования CTESK.
1. Введение Для удовлетворения непрерывно возрастающих потребностей пользователей и поддержки стабильного развития современного общества требуется разрабатывать всё более сложное программное обеспечение (ПО). Разработка всегда включает в себя множество циклов вида «тестирование — обнаружение ошибок — исправление ошибок — проверочное тестирование». Чем сложнее становится разрабатываемое ПО и чем более серьёзные задачи оно решает, тем больше в нём может происходить разнообразных внутренних и внешних взаимодействий, и в результате могут возникать всё более изощрённые ошибки. Некоторые из них проявляются только при сложных комбинациях внутренних и внешних условий, в результате их гораздо сложнее своевременно обнаруживать, локализовывать в реализации и исправлять, и тем выше их потенциальная опасность. Ряд технологий тестирования позволяют добиваться заданного уровня тестового покрытия. К ним относится, например, UniTESK [1-4] — технология автоматизации функционального тестирования на основе формальных методов, разработанная в Институте системного программирования РАН. Однако чем сложнее сама тестируемая система и разработанные для неё тесты, тем больше информации о наблюдаемом поведении приходится анализировать в случае обнаружении ошибки, и тем сложнее понять, какая часть этой информации имеет непосредственное отношение к обнаруженной ошибке, а какая — нет. Данная статья посвящена 137
нахождения
2. Постановка задачи Применяемые при тестировании методы зависят от сложности тестируемой системы. В простейших системах внутреннее состояние отсутствует или несущественно для поставленной задачи тестирования, а реакции, которые они выдают, однозначно определяются полученными стимулами. В этом случае при обнаружении ошибки для её воспроизведения достаточно повторить такой же тестовый стимул, а для её локализации в коде достаточно проверить код реализации, выполняющийся при обработке данного стимула. Но гораздо чаще поведение системы зависит не только от полученного стимула, но и от её внутреннего состояния, которое, в свою очередь, зависит от предыстории взаимодействий с системой. Тогда для воспроизведения ошибки необходимо тем или иным образом привести систему в то же состояние и применить тот же тестовый стимул, что и при её обнаружении, а для локализации — проверить код, обрабатывающий данный стимул в данном состоянии. Для тестирования таких систем широко применяется подход, при котором тестируемая система моделируется конечным автоматом (КА) [5]. Состояния автомата соответствуют состояниям этой системы, входной алфавит — набору всех возможных воздействий на систему (стимулов), выходной алфавит — набору возможных реакций системы, а начальное состояние — состоянию, в котором тестируемая система находится (или в которое приводится) в начале теста. Задача построения тестовых последовательностей при этом сводится к построению набора путей на графе переходов модельного КА, удовлетворяющего заданным требованиям к покрытию. Такой подход применяется и в технологии UniTESK. Для создания тестового сценария UniTESK необходимо описать только способ вычисления состояния модельного КА из состояния реализации (обычно одно модельное состояние соответствует целому классу реализационных), а для каждого модельного состояния — способ вычисления возможных в нём тестовых стимулов [6]. Всю работу по построению обхода графа переходов КА, обеспечивающего создание конкретной тестовой последовательности, берёт на себя библиотечный компонент тестовой системы, называемый обходчиком, который автоматически строит граф переходов модельного КА по мере его обхода [5,6]. Для проверки корректности работы тестируемой системы используются формальные спецификации требований к ней. Каждый раз после применения очередного тестового стимула полученные от тестируемой системы реакции анализируются на соответствие спецификации, и определяется новое состояние модельного КА. Несоответствие наблюдаемой реакции описанным 138
в спецификации требованиям или расхождения между ожидаемым и наблюдаемым состоянием целевой системы считается ошибкой и требует дальнейшего анализа. В результате анализа может быть принято решение о некорректности спецификации или о существовании ошибки в реализации. Поскольку зачастую состояние системы имеет достаточно сложную структуру, а единственный способ его изменить — подавать те же стимулы, которыми пользовался тест, то возникает естественный способ приведения системы в то же состояние для воспроизведения ошибки: точное повторение последовательности стимулов, которые подавал тест, обнаруживший эту ошибку. Последовательность стимулов при этом извлекается из сгенерированной тестом трассы, а процесс повторения тестовых стимулов называется воспроизведением трассы (trace replay). В силу используемого в технологии UniTESK способа описания модельного КА, этот способ надежнее повторного прогона тестов, так как в общем случае обходчик UniTESK не гарантирует тот же порядок обхода дуг графа. Более того, в реальных проектах часто нет возможности во время тестирования точно определить внутреннее состояние тестируемой системы. В этом случае она моделируется как черный ящик [6], состояние которого не получается напрямую из состояния тестируемой системы, а вычисляется на основании предыстории взаимодействий с ней. При этом после каждого тестового воздействия новое состояние модели вычисляется на основании предыдущего состояния, поданного стимула и полученных реакций. По разным причинам (это могут быть как ошибки реализации, так и ошибки спецификации) со временем состояние модели может перестать соответствовать состоянию реализации. Это приводит к расхождению между ожидаемым и наблюдаемым поведением тестируемой системы, причем такое расхождение может проявиться не сразу, а только в реакциях на последующие стимулы, то есть, момент совершения ошибки и момент её обнаружения могут различаться. В этом случае задачи воспроизведения и локализации ошибки существенно усложняются: поскольку у нас нет достоверного знания о внутреннем состоянии системы в момент проявления ошибки. В связи с этим может потребоваться воспроизвести всю последовательность тестовых воздействий из трассы теста, обнаружившего ошибку, и проверить весь код, который был активирован ими, и все элементы состояния реализации, которые были при этом затронуты. Поскольку к моменту обнаружения ошибки тест UniTESK мог проделать достаточно длинный и запутанный путь по графу КА, совершив много переходов и побывав в некоторых состояниях много раз, анализ на предмет ошибки всей предшествующей трассы становится слишком сложен, поэтому возникает необходимость поиска минимального теста, обнаруживающего ту же ошибку. Для решения этой задачи был разработан метод сокращенного воспроизведения трассы, описанный в данной статье. В технологии UniTESK тестовый сценарий содержит функцию вычисления состояния модельного КА и набор сценарных методов. Сценарный метод 139
содержит набор итерационных переменных (возможно пустой), правила перебора их значений и способ отображения набора значений итерационных переменных в конкретное тестовое воздействие. Входным алфавитом модельного КА считается при этом пара (имя сценарного метода, набор значений его итерационных переменных). В процессе выполнения тестовый сценарий UniTESK автоматически генерирует трассу, содержащую различную информацию о работе сценария, тестовой и тестируемой систем. Для рассматриваемого метода нас интересует следующая информация. 1. Запускаемые тестовые сценарии. 2. Модельное состояние сценария. 3. Вызываемые в рамках сценария сценарные методы и их распределение по потокам управления. 4. Значения итерационных переменных сценарных методов. Метод сокращенного воспроизведения трасс опирается на следующие гипотезы. I. Поведение тестируемой системы детерминировано относительно предыстории взаимодействий с ней. II. В случае обнаружения ошибки, с наибольшей вероятностью её причина лежит в том взаимодействии с тестируемой системой, в котором она была обнаружена, или в последних предшествующих ему взаимодействиях. III. Переход КА (поведение тестовой системы и изменение её состояния) однозначно определяется его состоянием, вызванным сценарным методом и значениями итерационных переменных этого метода. Эта гипотеза является технологическим требованием обходчика UniTESK, то есть, требованием корректности сценария. Рассмотрим гипотезу I подробнее. Поскольку наша задача — локализация уже заведомо существующей ошибки, детерминизм поведения в данном случае не означает гарантированного совпадения поведения тестируемой системы со столь же детерминированной спецификацией, описывающей зависимость полученного состояния системы и наблюдаемых выходных реакций от предшествующего состояния и полученных стимулов. Мы всего лишь предполагаем, что каждый раз при получении одной и той же последовательности стимулов тестируемая система будет выдавать одну и ту же последовательность реакций и переходить в одни и те же состояния; при этом с точки зрения спецификации её поведение может быть недетерминированным. Допустим, тестируемая система, находясь в состоянии, которому соответствует модельное состояние A, и получив модельный стимул S, в первый раз перешла в состояние A1 и выдала реакцию R1, а в другой раз, попав в эквивалентное состояние, также соответствующее модельному 140
состоянию A и получив тот же стимул S, перешла в состояние A2 и выдала реакцию R2, причем (A1, R1) (A2, R2). Тогда с точки зрения спецификации система демонстрирует недетерминированное относительно состояний и стимулов поведение. Но если при многократном воздействии одной и той же последовательности стимулов целевая система каждый раз проходит по тем же самым состояниям и выдает ту же последовательность реакций (в том числе (A1, R1) в первый раз и (A2, R2) во второй), то мы говорим о детерминизме её поведения относительно предыстории и предполагаем, что расхождение между наблюдаемым поведением и спецификацией определяется какими-то элементами состояния реализации, которые не описаны в спецификации, но изменились в промежутке между этими двумя моментами. Тогда для устранения ошибки необходимо локализовать эти элементы состояния и исправить работу с ними или исправить спецификацию. Чтобы облегчить работу по локализации обнаруженной тестом ошибки, можно найти минимальный набор предшествующих событий, после которого расхождение между реализацией и спецификацией гарантированно проявляется. Пример Тестируемая система — менеджер памяти со следующими методами: alloc() — выделение блока памяти, free() — освобождение блока и optimize() — оптимизация и дефрагментация выделенной памяти. Модельное состояние системы — объем распределённой памяти. Ситуация 1: метод free() на самом деле не освобождает память. Тогда мы можем получить следующую воспроизводимую последовательность событий. ... state=10 alloc() возвращает блок памяти state=11 alloc() возвращает NULL state=11 free() — Возникновение ошибки в реализации state=10 optimize() state=10 alloc() возвращает NULL — Проявление ошибки, недетерминизм Ситуация 2: в модели не учтено, что метод optimize() может существенно изменять состояния реализации, создавая достаточные для выделения участки непрерывной памяти. В результате мы имеем воспроизводимую последовательность событий. ... state=10 alloc() возвращает блок памяти 141
state=11 alloc() возвращает NULL state=11 free() state=10 optimize() — Существенное изменение состояния реализации, не отраженное в модели state=10 alloc() возвращает блок памяти state=11 alloc() возвращает блок памяти — Ошибка, недетерминизм В обоих случаях мы имеем тестируемую систему с детерминированным относительно предыстории поведением, которая ведёт себя недетерминировано с точки зрения модели. В первом случае это вызвано ошибкой в самой системе, а во втором случае — неточностью спецификации. Однако в обоих случаях расхождение между моделью и реализацией возникло в промежутке между двумя одинаковыми тестовыми стимулами, которые были применены в одинаковых состояниях (эти состояния и стимулы выделены жирным шрифтом), а потому с точки зрения модели должны были приводить к одинаковым результатам, но при тестировании привели к разным.
3. Определения Назовём трассой упорядоченный список состояний и переходов модельного КА, обладающий следующими свойствами. 1. Трасса или пуста, или начинается с состояния и заканчивается состоянием. 2. Состояния и переходы в трассе чередуются. 3. Каждый встретившийся в трассе переход переводит модельный КА из состояния, которое непосредственно ему предшествует в трассе, в состояние, непосредственно следующее в трассе за ним. Для удобства мы будем также использовать две сокращенные формы записи трассы: в одной будет опускаться конечное состояние трассы, в другой — все состояния. Пустая трасса в любой форме записи представляет собой пустой список. Стандартный тест UniTESK представляет собой один активный поток выполнения [7], в котором выполняется обход модельного КА. В процессе выполнения теста в трассу UniTESK последовательно сбрасываются достигнутые состояния КА и совершённые им переходы. В случае отсутствия сбоев в тестовой системе, можно в дальнейшем извлечь из файла трассы последовательный список состояний и переходов модельного КА; назовём этот список трассой теста. Очевидно, что трасса теста является трассой согласно данному выше определению и начинается в начальном состоянии 142
модельного КА. Тест UniTESK имеет два стандартных режима работы: в первом после обнаружения ошибки состояние модели тестируемой системы считается недостоверным, дальнейшие вердикты тестового оракула о корректности или некорректности поведения тестируемой системы также считаются недостоверными, поэтому тестирование на этом останавливается, и трасса завершается состоянием с ошибкой. Есть и второй режим, при котором в случае, если достоверно установлено, что нарушений в состоянии тестируемой системы и расхождений между её состоянием и состоянием модели нет, то тест продолжается и может даже обнаруживать другие ошибки. Но поскольку зачастую таких гарантий у нас нет, обычно используется первый режим. Так как рассматриваемый в статье метод минимизации тестов предназначен в первую очередь для сложных случаев, когда возможно не проявляющееся сразу расхождение между состояниями тестируемой системы и модели, мы будем считать недостоверной всю трассу после обнаружения первой ошибки. Трасса теста может содержать один и тот же переход КА несколько раз, но для работы с трассами (в силу возможного расхождения между модельным состоянием и истинным состоянием тестируемой системы, а также в соответствии с гипотезой II) нам необходимо различать вхождения одного и того же перехода КА в трассе. Поэтому переход трассы мы считаем экземпляром (instance) перехода КА, и в дальнейшем под переходом понимаем именно переход трассы. Поскольку для рассматриваемого метода неважен способ, которым совершается тот или иной переход в реализации, но важен порядок этих переходов, введём индексацию переходов трассы, пронумеровав их возрастающей последовательностью натуральных чисел. В силу гипотезы III, вся информация, необходимая для отображения индексов переходов в конкретные тестовые стимулы при последующем воспроизведении (полном или частичном) трассы, содержится в исходной трассе теста UniTESK. Список состояний и переходов T1 назовём подтрассой трассы T, если он является трассой согласно данному выше определению и его можно получить из T путём выбрасывания некоторых элементов списка без перестановки оставшихся (то есть, индексация входящих в подтрассу переходов остаётся возрастающей). Назовём ациклической трассу, в которой все состояния различны. Циклической трассой или циклом назовём такую трассу, у которой начальное и конечное состояния совпадают. Простым циклом назовём такую циклическую трассу, у которой совпадают только начальное и конечное состояние, а все остальные состояния отличны от них и попарно различны. Введём многоместную операцию сложения «+», отображающую наборы подтрасс одной исходной трассы, удовлетворяющие данному ниже предусловию операции, в подтрассу той же исходной трассы следующим образом: 143
1. 2. 3.
Нульместная операция «+» всегда возвращает пустую трассу. Одноместная операция «+» возвращает в качестве результата свой аргумент. Для двух и более аргументов определим сумму следующим образом: 3.1. Выпишем подряд все переходы из всех трасс, заданных в качестве аргументов операции, упорядочив их в порядке индексации. 3.2. Если в заданном наборе в разных трассах встречаются повторяющиеся индексы — ошибка, операция сложения к данному набору трасс неприменима. 3.3. Добавим в полученный список переходов символы состояний (один символ в начале списка, один в конце и по одному между каждыми двумя последовательными переходами) так, чтобы перед каждым переходом стояло состояние, в котором он начинается, а после каждого перехода — состояние, в котором он завершается. Если это сделать невозможно, то есть существуют два таких последовательных элемента полученного на шаге 3.1 списка переходов, что конечное состояние первого не совпадает с начальным состоянием второго — ошибка, операция сложения к данному набору трасс неприменима.
Предусловие операции сложения заключается в том, что приведённый алгоритм её вычисления не должен выдавать ошибку.
4. 4. Метод сокращенного воспроизведения трассы Рассмотрим следующую трассу теста: T = . Здесь буквами обозначены состояния модельного КА, числами — переходы, символом «звёздочка» — ошибка. Поскольку, как было указано выше, после обнаружения ошибки состояние тестируемой системы недостоверно, мы считаем состояние D* (модельное состояние D после обнаружения ошибки) отличным от состояния D. Отметим также, что в силу этого соглашения в трассе теста, обнаружившего ошибку, конечное состояние всегда отличается от начального. Графически эта трасса представлена на Рис. 1.
144
Очевидно, что трасса T2 не является простым циклом. Она разложима на сумму простых циклов, причем не единственным образом, как это показано на Рис. 2-4.
T= Рис. 1. Трасса выполнения теста. Рис. 2. Пример разложения трассы T.
Если гипотеза I выполняется, то мы можем воспроизвести всю трассу заново и убедиться, что ошибка также воспроизводится. Исходя из этой гипотезы, для локализации ошибки мы будем искать кратчайшую подтрассу трассы T, на которой ошибка всё ещё воспроизводится. В данном примере кратчайший путь из начального состояния в конечное очевиден: T1 = .
T=
Трасса T1 является кратчайшей подтрассой трассы T, ведущей из начального состояния в конечное. Мы можем воспроизвести эту подтрассу и проверить, повторяется ли при этом ошибка. В случае если повторить ошибку удалось, для её локализации достаточно исследовать подтрассу T1. В противном случае, для локализации ошибки требуется дальнейший анализ исходной трассы теста. Запишем трассу T в следующем виде: T = , B, 9, F, 10, D*>. Трасса T2 = (или, в другой форме записи, ) также является подтрассой трассы T, причем T = T1 + T2. Как мы здесь видим, трасса T2 вложена в трассу T1, то есть сначала воспроизводится часть трассы T1, затем её воспроизведение прерывается, целиком воспроизводится циклическая трасса T2, после чего воспроизведение трассы T1 продолжается с той же точки. Можно представить это как вызов подпрограммы в тесте. Будем также говорить в таких случаях, что подтрасса T2 вложена в подтрассу T1, причем отношение вложенности будем считать транзитивным. 145
Рис. 3. второй пример разложения трассы T.
T=
Рис. 4. Еще один пример разложения трассы T. Все приведенные на Рис. 2-4 разбиения трассы T2 представимы таким же образом, как и разбиение T = T1 + T2: начало одной подтрассы, прерывание её вызовом вложенного цикла и завершение исходной подтрассы (такие вызовы показаны на Рис. 2-4 тонкими стрелками). 146
Рассмотрим разбиение трассы теста на сумму своих непустых подтрасс T = T1 + T2 + … + TN-1 + TN, удовлетворяющее следующим требованиям. 1. Для k=1…N определена сумма трасс k = T1 + T2 + … + Tk-1 + Tk , то есть к ним применима операция сложения. 2. Трассы T1 … TN пронумерованы в порядке, обратном индексам последних переходов, входящих в эти трассы (то есть, i, j: 1 i < j N последний переход трассы Ti имеет индекс больший, чем последний переход из Tj) 3. Трасса T1 ведёт из начального состояния трассы T в конечное и является простым циклом или ациклична (поскольку нас интересуют только трассы тестов, обнаруживающие ошибку, трасса T1 будет для них ациклична) 4. Подтрассы T2 … TN — простые циклы. Требование 1 гарантирует возможность инкрементального наращивания подтрасс k. Требование 2 гарантирует, что каждый раз при добавлении к воспроизводимой части трассы очередной подтрассы Tk, k содержит всё более длинную непрерывную хвостовую цепочку переходов из исходной трассы теста, обеспечивая, согласно гипотезе II, инкрементальный поиск места, где была допущена ошибка. Требования 3 и 4 минимизируют шаг инкрементального поиска при наращивании воспроизводимой части трассы. Теорема 1. Для любой трассы существует разбиение на сумму подтрасс, удовлетворяющее требованиям 1-4. Доказательство. В Приложении 1 приведён алгоритм, строящий для любой трассы разбиение на подтрассы. Докажем, что полученное в результате его работы разбиение обладает всеми требуемыми свойствами, и что элементы этого разбиения являются трассами согласно определению. Введём на исходной трассе fullPath индексацию, обозначив каждый переход его порядковым номером в трассе. Лемма. В начале каждой итерации главного цикла алгоритма переменная currentPath содержит трассу: корректную (согласно определению трассы), с возрастающей индексацией переходов, ациклическую. При этом если текущий шаг — не последний, то присоединение к этой трассе очередного перехода из fullPath корректно. Очевидно, что в начале работы алгоритма утверждение Леммы выполняется, так как currentPath содержит пустой список переходов. Далее по индукции. Если в начале i-й итерации цикла эти условия выполняются, то после присоединения к currentPath очередного перехода получаем корректную трассу, причем, поскольку fullPath является корректной трассой, присоединение на следующем шаге очередного перехода, если он 147
существует, также будет корректным. Поскольку переходы входной трассы обрабатываются строго по порядку, индексация переходов также останется возрастающей. В начале шага алгоритма переменная currentPath содержала ациклическую трассу, то есть, все встречающиеся в ней переходы были попарно различны. После добавления очередного перехода может возникнуть цикл, то есть, конечное состояние этого перехода может совпадать с одним из ранее встречающихся в трассе currentPath состояний. Если такого совпадения не произошло, то, очевидно, утверждение Леммы также выполняется. Если произошло совпадение, то конечное состояние трассы currentPath могло совпасть только с начальным состоянием ровно одного из её переходов (это может быть и последний переход, если добавленный переход является петлёй). Таким образом, получившийся цикл — простой. Так как этот цикл является подсписком без перестановок содержащейся в currentPath трассы, его индексация будет возрастающей. После удаления полученного цикла из currentPath мы опять получаем ациклическую трассу с возрастающей индексацией, причем её конечное состояние при этом не меняется, то есть, в конце текущей итерации (и, соответственно, в начале следующей итерации) можно будет корректно добавить очередной переход исходной трассы. Таким образом, индуктивный переход доказан, и вся Лемма доказана. Из утверждения Леммы следует, что все элементы построенного разбиения будут корректными трассами. Одновременно мы доказали, что для построенного разбиения выполняется Требование 4. После последней итерации алгоритм добавляет при необходимости в начало разбиения оставшийся прямой путь (T1) из начального состояния исходной трассы в конечное. Если начальное и конечное состояния совпадают, то цикл T1 будет добавлен в начало разбиения во время последней итерации. Таким образом, Требование 3 также выполняется. На каждой итерации алгоритма к currentPath добавляется очередной переход из исходной трассы, и в случае обнаружения цикла этот переход входит в него. Полученный цикл добавляется в начало разбиения. Таким образом, обеспечивается выполнение Требования 2. Докажем, что Требование 1 также выполняется построенного алгоритмом разбиения. Очевидно, что для 1 = T1 оно выполняется. Рассмотрим шаг работы алгоритма, на котором к списку pathes добавляется элемент Tk (2kN). Непосредственно перед таким добавлением переменная currentPath содержит корректную трассу с возрастающей индексацией = S + Tk, где S соответствует некой цепочке переходов (возможно, пустой), с индексами, предшествующими индексам из трассы Tk. Обозначим индексы входящих в цикл Tk переходов через i1, …, i2. 148
Рассмотрим теперь шаг, на котором алгоритм обнаруживает следующий цикл, выделенный в подтрассу Tk-1 (элементы искомого разбиения строятся в обратном порядке). По построению видим, что индекс каждого перехода, входящего Tk-1 , или больше всех индексов, входящих в Tk , или меньше их всех. Выписав подряд в возрастающем порядке индексы всех переходов, входящих в любую из этих двух подтрасс, получаем возрастающий список индексов вида <j1, …, j2, i1, …, i2, j3, …, j4>, где индексы j1, …, j2 и j3, …, j4 (список j1, …, j2 может быть пуст) соответствуют переходам, принадлежащим трассе Tk-1, а i1, …, i2 — переходам, принадлежащим трассе Tk. По построению начальное состояние перехода i1 совпадает с конечным состоянием перехода j2, если он существует, или с начальным состоянием трассы Tk-1 в обратном случае, а конечное состояние перехода i2 совпадает с начальным состоянием перехода j3. Таким образом, для любого номера k=2..N определена сумма трасс Tk-1 + Tk, причем начальное и конечное состояния этой суммы трасс совпадают соответственно с начальным и конечным состояниями трассы Tk-1. Можно сказать также, что каждая следующая подтрасса в полученном разбиении или вложена в предыдущую, или непосредственно предшествует ей в исходной трассе. В силу доказанного свойства, не существует индекса, относящегося к трассе Tk-2 и лежащего между двумя индексами, относящимися к трассе Tk-1. Следовательно, добавление трассы Tk-2 к сумме трасс Tk-1 + Tk не нарушает предусловие операции сложения трасс, то есть, для k3 определена также и сумма трасс Tk-2 + Tk-1 + Tk, а её начальное и конечное состояние совпадают соответственно с начальным и конечным состояниями трассы Tk-2. Повторяя аналогичные рассуждения необходимое количество раз, мы доказываем даже более сильное утверждение относительно построенного алгоритмом разбиения: Для i, j: 1 i j N, следовательно, определена сумма трасс Ti + … + Tj. Следовательно, Требование 1 также выполняется для построенного разбиения. Теорема доказана. Доказав существование таких разбиений, мы можем пользоваться методом локализации ошибок путём сокращенного проигрывания трасс, который опирается на них. Метод заключается в следующем. 1. Считываем из трассы теста, обнаруживающего ошибку, список пройденных состояний и переходов и выкидываем из него все элементы, следующие за состоянием, в котором была обнаружена ошибка. 2. Строим разбиение полученной трассы на набор подтрасс, удовлетворяющий вышеописанным свойствам. 3. Находим минимальный номер k такой, что трасса k = T1 + T2 + … + Tk-1 + Tk находит ошибку, а трасса k-1 = T1 + T2 + … + T k-1 — не находит. Таким образом, мы обнаруживаем, что ошибка впервые 149
начинает проявляться после добавления к проигрываемой части трассы теста простого цикла Tk. Этот цикл естественным образом становится первым «подозреваемым» на внесение ошибки в состояние тестируемой системы. Как мы видели в примере выше, такое разбиение не единственно. Более того, различные разбиения одной и той же трассы, удовлетворяющие приведённым выше требованиям, могут содержать даже разное количество элементов, как показано на Рис. 5. Поскольку в общем случае мы не знаем заранее, где именно допущена ошибка в тестируемой реализации, мы тем более не можем также знать, какое из разбиений будет более полезным для локализации ошибки. Однако если выполняются гипотезы I и II, мы можем, тем не менее, ожидать, что разбиение, которое содержит большое количество коротких циклов, будет лучше разбиения, содержащего малое количество длинных.
Рис. 5. Пример разбиений одной трассы с разным числом элементов. На данный момент создана и апробирована реализация метода, использующая приведенный в Приложении 1 алгоритм и строящая с его помощью единственное разбиение.
5. Реализация Создана реализация предложенного метода для технологии CTESK. Инструментарий CTESK [8,9] реализует технологию UniTESK для языка Си, поддерживая спецификационное расширение языка Си SeC [10].
5.1. Описание инструмента Инструмент написан на языке Java и инсталлируется как дополнение к пакету программ UniTESK Trace Tools. Он принимает на вход набор файлов с трассами CTESK, анализирует их и для каждой трассы генерирует следующие выходные файлы. 150
Результат анализа графа трассы. Этот файл содержит следующую информацию. o Все обнаруженные во входной трассе состояния тестового сценария. o Информацию о первой обнаруженной в трассе ошибке (сценарий UniTESK в некоторых случаях может продолжать работу после ошибок, но в рассматриваемом методе оставшаяся часть трассы считается недостоверной и игнорируется). o Сигнатуры встретившихся во входной трассе сценарных методов. Сигнатура состоит из имени сценарного метода, а также типов и имён используемых в нём итерационных переменных. o Разбиение входной трассы на подтрассы согласно приведённым выше требованиям к такому разбиению. o Некоторая дополнительная информация, полученная в результате анализа графа состояний модельного КА. Эта информация не используется инструментом, но может быть полезна для разработчика. Сценарий, позволяющий воспроизводить записанную в трассе последовательность вызовов сценарных методов целиком или частично для поиска ошибок. Поскольку язык SeC не предусматривает непосредственного вызова сценарных методов с указанием значений итерационных переменных, сценарий генерируется на языке Си и работает совместно с результатом трансляции сценария на SeC в язык Си транслятором CTESK.
Инструмент налагает на входные трассы следующие ограничения. Трасса должна быть сгенерирована с помощью CTESK. Трасса содержит запуск ровно одного тестового сценария (работа с множеством сценариев, работающих последовательно или параллельно, пока что не реализована). Запущенный в трассе сценарий имеет тип DFSM (такой сценарий имеет единственный активный поток управления [7], в котором происходит обход детерминированного КА [6]). Некоторые дополнительные ограничения на внутреннюю целостность трассы, гарантирующие корректность извлеченной из неё информации о структуре модельного КА и сигнатурах вызываемых методов. При нарушении некоторых из этих условий инструмент выдает соответствующее сообщение об ошибке и прекращает обработку трассы, при нарушении других сгенерированный сценарий может не компилироваться или быть неработоспособным. Например, если подать на вход инструменту трассу, 151
сгенерированную с помощью JavaTESK, на выходе можно получить корректный анализ трассы (который может быть полезен разработчику) и не имеющий смысла сценарий на языке Си.
5.2. Сгенерированный сценарий Пример сгенерированного инструментом сценария приведён с некоторыми сокращениями в Приложении 2. Сценарий содержит следующие методы. replay_check_state(const char *state) — проверка текущего модельного состояния исходного сценария CTESK. replay_call__initialize(int argc, char **argv) — инициализация сценария CTESK. replay_call__finalize() — завершение сценария CTESK. replay_call__<имя_метода>(…) для каждого встретившегося во входной трассе сценарного метода — вызов соответствующего сценарного метода с заданными значениями итерационных переменных. Имена и типы параметров метода соответствуют именам и типам итерационных переменных исходного сценарного метода, с точностью до некоторых преобразований типов. do_replay(int path_number) — воспроизведение подтрассы path_number main_<имя_исходной_трассы>(int argc, char **argv) — главный метод сгенерированного сценария. parse_<тип>(char *val) для каждого встреченного в исходной трассе пользовательского типа итерационной переменной — декларация внешней пользовательской функции, восстанавливающей значения данного пользовательского типа из используемого в файле трассы строкового представления. Тела и сигнатуры сгенерированных методов согласованы с извлеченными из входной трассы сигнатурами вызванных сценарных методов, с реализуемой транслятором CTESK схемой генерации кода и с библиотекой поддержки тестовой системы времени выполнения TSBasis. Для реализации инструмента автор совместно с разработчиками инструментария CTESK доработал схему трансляции языка SeC: в неё была добавлена возможность вызова на уровне сгенерированного кода сценарных методов с точно указанными значениями итерационных переменных. Эти изменения вошли в очередную официальную версию CTESK. Для использования сгенерированного сценария его можно собрать (например, в проекте Microsoft Visual Studio с подключенным модулем интеграции CTESK) вместе со сценарием, сгенерировавшим исходную трассу, и со всем, что необходимо для его исполнения (спецификации, медиаторы, тестируемая система, библиотека поддержки выполнения тестов TSBasis, внешние 152
модули). После этого можно (а иногда необходимо) доработать сгенерированный сценарий (такая доработка необходима, если в исходном сценарии использовались итерационные переменные сложных спецификационных типов; в противном случае, сценарий работоспособен сразу после создания), заменить в процедуре main() запуск исходного сценария CTESK на вызов процедуры main_…() сгенерированного сценария, откомпилировать и запустить проект.
5.3. Запуск сценария При запуске сценарий ищет в параметрах командной строки параметр -path <path_number>. Если параметр найден и имеет положительное значение в диапазоне 1 path_number MAX_PATH_NUMBER , то воспроизводится подтрасса path_number. Если параметр отсутствует, то воспроизводится подтрасса DEFAULT_PATH_NUMBER. При значении path_number = 0 сценарий переходит в режим автоматической локализации ошибок методом инкрементального поиска — он последовательно перебирает все значения этого параметра от 1 до максимально возможного для построенного разбиения и останавливается после первой обнаруженной ошибки. При этом перед воспроизведением каждой подтрассы состояние тестовой системы инициализируется методом init() исходного сценария, а после воспроизведения сбрасывается методом finish(), если таковые в нём определены. Остальные параметры командной строки обрабатываются тестовой системой и исходным тестовым сценарием. В ходе выполнения сценарий генерирует трассу UniTESK, которую разработчик может впоследствии анализировать. Перед воспроизведением подтрассы k сценарий сбрасывает в выходную трассу сообщение Scenario replay: trying path . При наличии в исходной трассе ошибок, в полученной при запуске сгенерированного сценария трассе обязательно встречается одно из следующих сообщений: Scenario replay: repeatable failure — если при воспроизведении подтрассы удалось воспроизвести ожидаемую ошибку; Scenario replay: could not repeat failure — если ожидаемую ошибку воспроизвести не удалось. Также независимо от наличия ошибок в исходной трассе возможно появление сообщения Scenario replay: unexpected failure — в случае, если при работе сценария возникла неожиданная ошибка. В режиме автоматической локализации ошибок в выходную трассу сценария дополнительно сбрасываются сообщения следующего вида: Scenario replay: failure found at path — если ошибка впервые возникла при воспроизведении подтрассы k; Scenario replay: could 153
not repeat failure at any path — если ошибку не удалось воспроизвести ни на одной из подтрасс k Ошибкой считается возникновение любого из следующих событий. 1. Вызванный сценарный метод возвращает сообщение об ошибке (оно может быть вызвано сообщением об ошибке от тестового оракула или нарушением внутренних условий целостности тестового сценария). Такая ошибка считается ожидаемой, если она произошла в том же переходе, что и в исходной трассе; в противном случае, ошибка считается неожиданной. Какие-либо другие характеристики ошибки при этом не проверяются. 2. При воспроизведении трассы тестируемая система пришла в состояние, отличное от ожидаемого. Такая ошибка не может быть ожидаемой. При возникновении любой ошибки, соответствующая информация сбрасывается в выходную трассу, а работа сценария завершается.
6. Апробация Описываемый метод был опробован на примерах, поставляемых вместе с инструментами CTESK. Примеры состоят из реализации небольшой системы, формальных спецификаций, медиаторов [4] и тестовых сценариев. Для апробации в реализацию, спецификации и тестовые сценарии (для технологии UniTESK несущественно, где именно возникла ошибка) вносились ошибки следующих видов. 1. Неверный результат работы метода при определённой комбинации состояния и входных параметров. 2. Вызов одного целевого метода с определёнными параметрами «портит» состояние так, что последующий вызов другого метода работает с ошибкой. 3. Накапливающаяся ошибка, которая проявляется только после определённого числа вызовов целевого метода. Все эти ошибки обнаруживались тестовыми сценариями CTESK, при этом генерировались соответствующие трассы тестов, которые потом обрабатывались инструментом и воспроизводились с использованием той же ошибочной реализации, а также с другими реализациями (без ошибок и с другими ошибками). Как и следовало ожидать, наилучшие результаты достигнуты для ошибок первого вида: сгенерированный из трассы сценарий успешно находил эти ошибки на кратчайшем пути от начального состояния к ошибочному (1). Ошибки второго вида обнаруживались на любой подтрассе k , начиная с первой включающей вызов, вносящий ошибку (вызов, обнаруживающий ошибку, всегда входит в эти подтрассы, так как расположен в исходной трассе 154
позже). Даже для ошибок третьего вида иногда удавалось сократить трассу, выкинув из неё находящиеся в начале трассы циклы, не участвующие в накоплении ошибки; но в целом для таких ошибок предлагаемый метод малоэффективен, поскольку минимальный тест, находящий ошибку, должен содержать все те взаимодействия с целевой системой из исходного теста, в которых ошибка накапливалась, а они обычно более-менее равномерно распределены по трассе теста, то есть не выполняется гипотеза II. Инструмент также испытывался на трассах тестов, полученных из проекта верификации ОС Linux OLVER [11] и ОС реального времени для встроенных систем «Багет». Для этих проектов в ИСП РАН были разработаны тесты, моделирующие разнообразные части ядра ОС и стандартных библиотек конечными автоматами с состояниями различной сложности: от вырожденных до имеющих весьма сложную структуру. Наиболее простым и эффективным было применение инструмента для подсистем, имеющих вырожденное состояние (например, семейство функций …printf()), но в целом для таких случаев ценность рассмотренного метода невелика, поскольку тестируемые функции не зависят от предыстории, и возникновение ошибки зависит только от входных параметров, которые можно извлекать из отчетов UniTESK напрямую. В случае сложного внутреннего состояния системы (например, сценарии, работающие с подсистемами mutex, pthread) сгенерированные инструментом файлы анализа во многих случаях достаточно наглядно описывали внутреннюю структуру состояния, в котором была обнаружена ошибка, и способ его получения. При работе с этими подсистемами было обнаружено еще одно ограничение, налагаемое инструментом на тесты: эквивалентные состояния модельного КА, в том числе полученные из разных запусков, должны иметь одинаковое строковое представление. Например, если состояние моделируется неупорядоченным множеством, то сравнение двух состояний как объектов может сообщать об их равенстве, а сравнение их строковых представлений — о неравенстве, так как порядок вывода элементов множества стандартными библиотечными функциями не регламентирован. Так как инструменту доступно только строковое представление состояния из входной трассы, для его применения в таких случаях необходима соответствующая доработка исходного сценария (написание детерминированных преобразований в строковое представление) или сгенерированного сценария (например, с помощью пользовательских функций, восстанавливающих значения соответствующих типов из строкового представления). Однако даже несмотря на эти ограничения, в некоторых случаях удавалось быстро получить сокращенный тест, находящий ту же ошибку.
7. Заключение В данной статье описан метод сокращения тестов, находящих ошибку, предназначенный для помощи разработчикам в анализе результатов 155
тестирования и локализации обнаруженных ошибок. Приведённый метод предназначен в первую очередь для тестирования систем, обладающих сложным и недоступным для теста напрямую внутренним состоянием. Метод сокращенного воспроизведения трасс позволяет автоматически обнаруживать тестовые воздействия, которые с большой вероятностью ответственны за возникновение ошибки в тестируемой системе, даже если проявления ошибки были зафиксированы позже. Приведено математическое обоснование предложенного метода. Предложен алгоритм, реализующий его, и доказана корректность этого алгоритма. Разработан и апробирован инструмент, реализующий предложенный метод для технологии CTESK [8], которая реализует унифицированную технологию тестирования UniTESK [1] для языка Си. Результаты апробации показывают хорошие перспективы применения данного метода в промышленных проектах. В дальнейшем планируется развивать предложенный метод в следующих направлениях. Разработка аналогичных инструментов для других языков, поддерживаемых технологией UniTESK. Создание библиотечных компонентов, упрощающих работу с пользовательскими типами данных. Реализация возможности работы с несколькими последовательными сценариями в одной трассе. Исследование возможности работы с параллельно работающими в одной трассе сценариями. Исследование других стратегий нахождения минимальных подтрасс, обнаруживающих заданную ошибку. Исследование возможности обработки более чем одной ошибки исходной трассы. Литература [1] http://www.unitesk.com/ru/ — сайт, посвященный технологии тестирования UniTesK и поддерживающим ее инструментам. [2] В. В. Кулямин, А. К. Петренко, А. С. Косачев, И. Б. Бурдонов. Подход UniTesK к разработке тестов. Программирование, 29(6):25–43, 2003. http://www.ispras.ru/~kuliamin/docs/Unitesk-2003-ru.pdf [3] I. Bourdonov, A. Kossatchev, A. Petrenko, and D. Galter. KVEST: Automated Generation of Test Suites from Formal Specifications. FM'99: Formal Methods. LNCS 1708:608–621, Springer-Verlag, 1999. [4] I. Bourdonov, A. Kossatchev, V. Kuliamin and A. Petrenko. UniTesK Test Suite Architecture. Proceedings of FME 2002, LNCS 2391:77–88, Springer-Verlag, 2002. http://www.ispras.ru/~kuliamin/docs/FME-2002-en.pdf [5] И. Б. Бурдонов, А. С. Косачев, В. В. Кулямин. Использование конечных автоматов для тестирования программ. Программирование, 26(2):61–73, 2000. http://www.ispras.ru/~kuliamin/docs/FSM-2000-ru.pdf
156
[6] И. Б. Бурдонов, А. С. Косачев, В. В. Кулямин. Неизбыточные алгоритмы обхода ориентированных графов: детерминированный случай. Программирование, 29(5):59–69, 2003. http://www.ispras.ru/~kuliamin/docs/Graphs-2003-ru.pdf [7] С. Г. Грошев. Применение технологии UniTesK для тестирования систем с различной конфигурацией активных потоков управления. Труды Института Системного Программирования РАН, 9:67–81, 2006. http://www.citforum.ru/SE/testing/unitest_use/ [8] CTESK. Документация. http://www.unitesk.ru/content/category/7/18/51/ [9] CTESK. Automating Testing of C Applications. Official CTESK whitepaper. http://www.unitesk.ru/download/papers/ctesk/ctesk_wp.pdf [10] CTESK 2.2: Описание языка SeC. http://www.unitesk.ru/download/papers/ctesk/CTesK2.2LanguageReference.rus.pdf [11] OLVER — проект по верификации ОС Linux. http://linuxtesting.ru/project/olver
Приложение 1. Алгоритм разбиения трассы В данном разделе приведёна реализация предложенного алгоритма на языке Java версии 1.5. В модели данных определен класс State, моделирующий состояние КА, и класс Arc, моделирующий переход трассы. Класс Arc содержит поля startState и endState типа State, ссылающиеся соответственно на начальное и конечное состояние соответствующего перехода. Входные данные:
List fullPath — исходная трасса (сокращенная форма записи, в которой фигурируют только переходы). Согласно определению трассы, для любых двух последовательных переходов трассы A1 и A2 , A1.endState == A2.startState
for (int j = 0; j < currentPath.size(); j++) { // Поиск цикла if (currentArc.endState == currentPath.get(j).startState) { // Выделяем обнаруженный цикл List newLoop = currentPath.subList(j, currentPath.size()); // Добавляем его в начало списка pathes pathes.add(0, newLoop.toArray(new Arc[newLoop.size()]) ); // Удаляем входящие в него переходы из currentPath newLoop.clear(); break; } } } // Добавляем в список pathes оставшийся прямой путь из начального состояния в конечное if (currentPath.size() > 0) pathes.add(0, currentPath.toArray(new Arc[currentPath.size()]) );
Приложение 2. Пример использования инструмента Дан следующий исходный сценарий CTESK, тестирующий реализацию банковского счета. AccountModel acct; static bool account_init (int argc, char **argv) {...} static Integer* account_state() { return create_Integer(acct.balance); } scenario bool deposit_scen() { if (acct.balance <= 5) { iterate (int i = 1; i <= 5; i++;) { deposit_spec(&acct, i); } } } scenario bool withdraw_scen() { iterate (int i = 1; i <= 5; i++;) withdraw_spec(&acct, i); return true; }
Выходные данные:
List pathes — искомое разбиение входной трассы на последователь-ность подтрасс.
Код реализации алгоритма приведен ниже. import java.util.*; ... List pathes = new LinkedList(); // Хранилище для результатов алгоритма List currentPath = new ArrayList(); for (int i = 0; i < fullPath.size(); i++) { // Главный цикл алгоритма Arc currentArc = fullPath.get(i); currentPath.add(currentArc); 157
158
scenario dfsm account_scenario = { .init = account_init, .getState = (PtrGetState)account_state, .actions = { deposit_scen, withdraw_scen, NULL } }; В реализацию внесена ошибка. Сценарий обнаруживает её и генерирует соответствующую трассу. Из этой трассы инструмент генерирует сценарий на языке Си следующего вида. #define DEFAULT_PATH_NUMBER 1 #define MAX_PATH_NUMBER 66 #define GOOD_CALL(call) if (!(call)) goto __unexpected_failure; #define BAD_CALL(call) if (call) goto __no_expected_failure; else goto __expected_failure; static static {...} static static static
bool replay_check_state(const char *state) {...} bool replay_call__initialize(int argc, char **argv) bool replay_call__finalize() {...} bool replay_call__deposit_scen(int i) {...} bool replay_call__withdraw_scen(int i) {...}
static int do_replay(int path_number) { // Straight path // pseudo-call: replay_check_state("start") // pseudo-call: replay_call__initialize() // transition #0 if (path_number > 16) { GOOD_CALL(replay_check_state("0")) GOOD_CALL(replay_call__deposit_scen(1)) // transition #1 } if (path_number > 61) { GOOD_CALL(replay_check_state("1")) GOOD_CALL(replay_call__deposit_scen(1)) // transition #2 } if (path_number > 62) { ... } if (path_number > 63) { ... }
159
if (path_number > 64) { GOOD_CALL(replay_check_state("4")) GOOD_CALL(replay_call__deposit_scen(1)) // transition #5 } if (path_number > 65) { GOOD_CALL(replay_check_state("5")) GOOD_CALL(replay_call__deposit_scen(1)) // transition #6 GOOD_CALL(replay_check_state("6")) GOOD_CALL(replay_call__withdraw_scen(1)) // transition #7} } if (path_number > 64) { GOOD_CALL(replay_check_state("5")) ... } if (path_number > 63) { ... } ...... if (path_number > 2) { ... } if (path_number > 1) { GOOD_CALL(replay_check_state("1")) GOOD_CALL(replay_call__deposit_scen(1)) // transition #400 GOOD_CALL(replay_check_state("2")) GOOD_CALL(replay_call__deposit_scen(1)) // transition #401 GOOD_CALL(replay_check_state("3")) GOOD_CALL(replay_call__deposit_scen(1)) // transition #402 GOOD_CALL(replay_check_state("4")) GOOD_CALL(replay_call__deposit_scen(5)) // transition #403 GOOD_CALL(replay_check_state("9")) GOOD_CALL(replay_call__withdraw_scen(1)) // transition #404 GOOD_CALL(replay_check_state("8")) GOOD_CALL(replay_call__withdraw_scen(1)) // transition #405 GOOD_CALL(replay_check_state("7")) GOOD_CALL(replay_call__withdraw_scen(1)) // transition #406 GOOD_CALL(replay_check_state("6")) GOOD_CALL(replay_call__withdraw_scen(5)) // transition #407 } // Straight path GOOD_CALL(replay_check_state("1")) GOOD_CALL(replay_call__deposit_scen(1)) // transition #408 GOOD_CALL(replay_check_state("2")) GOOD_CALL(replay_call__deposit_scen(1)) // transition #409 GOOD_CALL(replay_check_state("3")) GOOD_CALL(replay_call__deposit_scen(1)) // transition #410 GOOD_CALL(replay_check_state("4"))
160
// Original error: info = 'Scenario function failed', kind = 'Scenario Function Failed' BAD_CALL(replay_call__deposit_scen(1)) // transition #411 /* The rest of replay is skipped */ return 0; __unexpected_failure: traceException("Scenario replay: unexpected failure"); return 1; __no_expected_failure: traceException("Scenario replay: could not repeat failure"); return 2; __expected_failure: traceUserInfo("Scenario replay: repeatable failure"); return 3; } int main_account_scenario_2007_06_15_11_00_01(int **argv) { ... }
argc,
char
Из приведённого отрывка кода мы можем, например, видеть, что переходы 408-411 принадлежат подтрассе T1, причем переход 411 вызывал в исходной трассе ошибку. Кроме того, подтрасса T66 состоит из переходов с индексами 6 и 7, начинается в состоянии, имеющем строковое представление «5» и заканчивается в нём же, вложена в T65 и не содержит других вложенных подтрасс; подтрассы T65, T64, T63 и T62 последовательно вложены друг в друга. Что касается подтрасс T62…T18, то они вложены напрямую в T17; такого рода ситуации возникают, когда несколько подряд циклов (не обязательно простых) начинаются в одном и том же состоянии. Более подробный анализ входной трассы содержится в файле анализа, который включает описание каждой подтрассы Tk в виде списка входящих в неё состояний и переходов и некоторую дополнительную информацию о графе КА, извлеченном из исходной трассы.
161
Тестирование современных библиотек тригонометрических функций Е. С. Чернов, В. В. Кулямин {ches, kuliamin}@ispras.ru Аннотация. Данная статья посвящена созданию тестового набора по методу, описанному в работе [1], для четырех тригонометрических функций (cos, tan, arccos, arctan) на основе существующих стандартов и дополнительных требований, возникающих из математических особенностей этих функций и специфических свойств представления чисел с плавающей точкой. Здесь также представлены результаты тестирования различных реализаций этих функций в математических библиотеках на разных платформах и при различных режимах округления.
1. Введение Одной из областей человеческой деятельности, опирающейся на использование сложного программного обеспечения (ПО), является математическое моделирование сложных явлений. Это, например, моделирование развития вселенной в целом, галактик и звезд, физических процессов в экстремальных условиях, биохимических, климатических и социальных процессов. Во многих случаях компьютерное моделирование дает важную информацию о таких явлениях, но очень нелегко получить независимую оценку правильности получаемых результатов, что позволило бы проверить корректность самого используемого ПО. Это порождает серьезные проблемы при разработке надежных систем математического моделирования. Тем не менее, повысить надежность и правильность работы таких систем можно за счет формальной проверки корректности библиотечных компонентов, на которые оно во многом опирается, в частности, реализаций математических функций. Уверенность в надежности фундамента, на котором построены эти системы, даст возможность разрабатывать их более качественно и с меньшими усилиями, сосредоточившись на поиске и исправлении ошибок в других компонентах. Правильность работы библиотек обычно определяется стандартами. Но большинство имеющихся стандартов, которые должны определять требования к реализациям математических функций, почти не содержат таких требований, ограничиваясь общими указаниями и апеллируя к широкой известности свойств этих функций. При попытке детализировать подразумеваемые требования оказывается, что 161
свойства реализаций функций существенно отличаются от свойств самих функций из-за использования чисел с плавающей точкой. В связи с этим актуальной является работа по определению четких требований к работе реализаций математических функций на основе чисел с плавающей точкой, а также разработка тестовых наборов для проверки соответствия этим требованиям. Этим двум вопросам и посвящена данная статья. Во втором разделе статьи производится обзор существующих стандартов для реализаций математических функций и существующих тестовых наборов. В третьем разделе проводится исследование свойств 4-х тригонометрических функций и на его основе строится тестовый набор для них. В четвертом разделе представлены результаты тестирования нескольких различных реализаций выбранных функций с помощью созданного набора.
2. Обзор стандартов и тестовых наборов Правильность работы реализаций математических функций определяется стандартами. Рассмотрим несколько наиболее распространенных стандартов для математических функций и требования, которые они предъявляют к работе их реализаций.
2.1. Существующие стандарты для математических функций Общепринятые базовые стандарты на вычисления с числами с плавающей точкой — это IEEE 754 [2] и IEEE 854 [3]. Они определяют представление чисел с плавающей точкой и основные операции над ними. Помимо обычных чисел, числа с плавающей точкой могут представлять положительную и отрицательную бесконечности (±∞), а также NaN («not-a-number» — «не число», приписываемое как результат действиям, у которых нельзя определить корректно ни конечный, ни бесконечный результат) и -0 (отрицательный ноль, используемый иногда в значении «очень маленькое по абсолютной величине отрицательное число»). Кроме того, обычные числа могут быть нормализованными (если их абсолютная величина достаточно велика, например, при двойной точности — не меньше 2-1022) или денормализованными (если их абсолютная величина меньше некоторого, своего для каждой точности, значения). Для получения представимых результатов при выполнении вычислений с числами с плавающей точкой эти стандарты предписывают использовать один из четырех режимов округления: к ближайшему числу, вверх, вниз и к нулю. Они также предписывают возвращать правильно округленный точный результат при выполнении арифметический действий (сложения, вычитания, умножения, деления), преобразовании типов, взятии остатка по модулю и извлечении квадратного корня. Про другие функции в этих стандартах ничего не сказано. 162
Стандарты библиотек языка C. В стандарте языка C ISO/IEC 9899 [4] и в стандарте переносимого интерфейса операционной системы IEEE 1003.1 [5] (известном как POSIX), описывающих библиотеку математических функций языка C, так же мало говорится о других функциях. Стандарт языка С ссылается на стандарт IEEE 754, добавляя лишь требования на поведение ряда функций в некоторых особых точках. Например, exp(0) = 1, а sin(0) = 0. Стандарт POSIX, в свою очередь, ссылается на требования стандарта языка С, добавляя лишь требования на выставление флагов в случае возникновения переполнения, слишком маленьких или слишком больших аргументах, а также на поведение функции при денормализованных аргументах, в нуле, в бесконечности и в NaN. К примеру, для функции синус POSIX требует, чтобы sin(x) = x при x = 0, -0, а также при денормализованных значениях x. Значение синуса для NaN, -∞ и +∞ должно быть NaN. Кроме того, указаны случаи выставления флага выхода за нормализованные значения (range error) и флага недопустимого аргумента (domain error). Других ограничений на синус не накладывается. Стандарты ISO/IEC 10967. В последние 5-10 лет появились предложения стандартизовать необходимые для аккуратного моделирования требования к реализациям математических функций [6,7]. Многие инициаторы этой деятельности работают в проекте Arenaire [8], совместно проводимом во Франции INRIA, CNRS и Высшей Нормальной школой Лиона. В результате появился набор стандартов ICO/IEC 10967 [9-11], формулирующий естественные ограничения на работу реализаций элементарных функций — корней, экспонент и логарифмов с различными основаниями, гиперболических и тригонометрических функций, а также обратных к ним. Эти ограничения касаются нескольких аспектов: точности вычислений, интервалов сохранения знака и монотонности, а также поведения функции в окрестности некоторых особых для данной функции точек (например, реализация экспоненты для значений аргументов, достаточно близких к 0, должна возвращать в точности 1). Эти стандарты на сегодняшний день являются наиболее полными и строгими, однако, к сожалению, они не используются на практике, поскольку не поддерживаются никем из основных производителей программных или аппаратных реализаций математических функций. Кроме того, набор стандартов ICO/IEC 10967 ничего не говорит о неэлементарных функциях, к которым относятся некоторые часто используемые библиотечные функции, например, гамма-функция и функции Бесселя.
2.2. Работы по тестированию реализаций математических функций Существует достаточно большое количество различных наборов тестов для тестирования функций над числами с плавающей точкой (см. например, [12]), 163
но подавляющее большинство таких тестов крайне несистематично и проверяет какой-то один аспект вычислений. Несмотря на то, что стандартизация вычислений над числами с плавающей точкой началась более 20 лет назад, проблема проверки соответствия реализаций стандартам до сих пор актуальна и тесты на правильность поведения реализаций математических функций по-прежнему необходимы. Среди наиболее систематичных работ по тестированию вычислений с плавающей точкой можно назвать следующие. Тестовый набор ELEFUNT [13] содержит тесты для нескольких математических функций в виде программ на C и Java. В этих тестах проверяется корректность возвращаемых значений для специальных значений аргументов (0, 1, +, -, NaN), а также для ряда генерируемых случайно значений аргументов проверяется выполнение некоторых тождеств, связанных с данной функцией (например, exp(x)·exp(-x) = 1). Тестовый набор UCBTEST [14] предназначен для тестирования базовых арифметических действий и достаточно широкого набора математических функций (тестируется больше функций, чем в ELEFUNT). Он оформлен как набор тестовых программ на Fortran и C и предопределенных входных данных для разных функций. В качестве тестовых данных, по-видимому, выбраны специальные значения аргумента и несколько случайным образом полученных значений. В целом, однако, в этом наборе используется больше данных и проверяется больше функций, чем в ELEFUNT. Использование в качестве тестовых данных специальных значений, границ интервалов, определяемых часто используемыми алгоритмами вычисления данной функции, а также чисел, построенных по некоторым шаблонам и случайных значений, применялось для построения более объемных тестовых наборов, например, набора Беркли [15]. Работы, выполнявшиеся в проекте Arenaire. Отдельно стоит отметить работы в рамках проекта Arenaire [16] по определению чисел, для которых корректное вычисление элементарных функций с заданной точностью наиболее трудоемко. На основе их результатов был разработан инструмент MPCheck [17] для тестирования правильности реализаций элементарных функций с точки зрения сохранения монотонности и корректности округления. В целом, имеющиеся в открытом доступе исследования, посвященные выработке требований к реализациям математических функций и тестированию на соответствие этим требованиям, не содержат систематического подхода к этим вопросам, объединяющего рассмотрение всех указанных проблем. Нигде, кроме работ группы Arenaire, не 164
рассматривается дилемма составителя таблиц (необходимость использовать вычисления повышенной точности для корректного округления) и значения аргументов, для которых вычисление корректных результатов функций наиболее трудоемко. В то же время, в работах группы Arenaire никак не фигурируют тестовые данных, основанные на границах интервалов, в которых заданная функция ведет себя однородным образом.
Специальные значения, значения в 0, касательные и асимптоты. o Бесконечности и -0. Нужно наиболее естественным образом определить значения функции для особых значений аргумента: -0, +∞, -∞. o Значение в NaN. Значение функции для аргумента NaN должно быть равно NaN. o Точные значения. В ряде точек значения функций должны вычисляться точно. Например, exp(0) = cos(0) = ch(0) = 1, sin(0) = sh(0) = tg(0) = arcsin(0) = 0, ln(1) = 0 и т.п. o Окрестности экстремумов, являющихся точными значениями. Если в такой точке производная функции равна 0, то для любого аргумента из некоторой ее окрестности реализация должна возвращать то же самое значение. o Окрестность 0. Функции, имеющие ненулевое значение в 0, тоже должны в некоторой окрестности 0 возвращать это же значение. o Горизонтальные асимптоты. В тех случаях, когда функция имеет горизонтальные асимптоты, необходимо аккуратно определить границы, после которых ее значение должно стать постоянным при определенном режиме округления. o Асимптотики. На интервалах, где основные асимптотики функций при правильном округлении превращаются в тождества, следует требовать соблюдения этих соотношений. Например, exp(x) ~ 1+x при x ~ 0 или sin(x) ~ x при x ~ 0.
Область значений функции. o Выбор ветви. Если математическая функция в строгом смысле является многозначной, должна быть четко определена ее ветвь, которая будет соответствовать реализации. o Сохранение ограничений области значений. Ограничения сверху или снизу на значения функции нужно соблюдать и в ее реализации, в противном случае возможны ложные нарушения непрерывности поведения при моделировании сложных систем. o Денормализованные значения. Необходимо аккуратно определить интервалы, на которых значения функции должны быть денормализованными.
Монотонность и сохранение знака. o Сохранение монотонности. На всех интервалах, где математическая функция монотонна, ее реализация должна иметь тот же вид монотонности.
3. Анализ требований к функциям и создание тестового набора 3.1. Методика определения требований Используемый метод определения требований к реализациям математических функций описан в статье В. Кулямина [1] и заимствует большую часть идей из стандарта ISO 10967 [9-11] и работ [6,7], посвященных разработке стандартов с повышенными требованиями к корректности вычисления математических функций. Требования к результатам реализации математической функции могут быть разделены на несколько аспектов. Область определения функции и особые точки функции. o Область определения. Во всех точках области определения, где значения функции лежат в интервале чисел с плавающей точкой, ее реализация должна возвращать соответствующим образом округленные значения. o Предельные значения. В точках, где функция не определена, но имеет предел в диапазоне чисел с плавающей точкой, реализация должна возвращать округленное значение этого предела. o Односторонние пределы в 0. Значения функции в точках 0, -0 определяются как односторонние пределы. Например, сtg(0) = +∞ и ctg(-0) = -∞. o Полная неопределенность. В точках, где функции нельзя осмысленно приписать ни конечного, ни бесконечного значения, ее значением считается NaN. Примеры таких ситуаций: sqrt(-1.0) = ln(-1.0) = NaN, sin(+∞) = NaN. o Окрестности полюсов. Необходимо точно определить окрестности полюсов, в которых значение функции выходит за интервал чисел с плавающей точкой. o Окрестности бесконечностей при бесконечных пределах. Для функций, стремящихся к бесконечности при x +∞ или x -∞, должны быть точно определены границы, за пределами которых их значения не представимы.
165
166
o
функций на базе чисел с плавающей точкой, нужно вычислить числа с плавающей точкой, приближающие подобные числа наилучшим образом. Это можно сделать с помощью одной и той же техники (упоминаемой, например, в [18,19]). Любое число с плавающей точкой, большее 1, может быть представлено как m·2k, где m — натуральное число, не превосходящее 253-1, а k — натуральное число, не превосходящее 971. Если такое число расположено очень близко к n·α, где α — иррациональное число, то α m·2k/n. Таким образом, можно искать эти числа с помощью наилучших рациональных приближений к α. Для этого числа вида α·2k при k от 52 до -971 раскладываются в непрерывные дроби, при обрыве которых в какомлибо месте получаются подходящие дроби, наилучшим образом приближающие исходные числа среди всех рациональных чисел, имеющих меньшие или такие же знаменатели [20].
Сохранение знака. Знак значения реализации функции для некоторого аргумента должен совпадать со знаком значения самой функции.
Симметрии и периодичность. o Четность и нечетность. Если математическая функция является четной или нечетной, этим же свойством должна обладать ее реализация. o Другие симметрии. Помимо четности и нечетности все другие симметрии функции должны быть проанализированы на предмет возможности их соблюдения для чисел с плавающей точкой. Например, соотношение Γ(1+x) = x·Γ(x) для гамма-функции может быть выполнено точно только для представимых целых чисел, для них оно и должно быть выполнено в любой реализации. Соотношения типа периодичности тригонометрических функций, или, например, sin(π-x) = sin(x), могут выполняться на числах с плавающей точкой только приближенно, поэтому не имеет смысла накладывать соответствующие ограничения.
Такие вычисления дают около 1000 кратных для каждого из чисел π/4, π/3, π/2 и π, которые приближаются числами с плавающей точкой двойной точности, с абсолютной погрешностью не больше, чем 2-52. Эти числа с плавающей точкой являются ближайшими к нулям, полюсам, максимумам, минимумам или к прообразам значений ±1 и ±1/2 для косинуса и тангенса. Поэтому они являются хорошими тестовыми значениями.
Корректное округление. Помимо всех перечисленных ограничений, нужно требовать, чтобы результат, возвращаемый реализацией, получался из точного результата функции для данного аргумента при помощи округления в соответствии с текущим режимом.
Например, так можно вычислить число двойной точности, ближайшее к нечетному кратному π/2 — 1.01101010110001011011001001100010110010100001111111112·2849. Поскольку нечетные кратные π/2 являются полюсами тангенса, вычислив его значение в этой точке, получаем максимальное по абсолютной величине его значение на числах двойной точности — 2.13348538575370393610·1018. Так как кратные π/2 являются нулями косинуса, его значение в этой точке дает минимальное по абсолютной величине значение косинуса на числах с плавающей точкой двойной точности вне окрестности нуля — 4.68716592425462669110·10-19.
3.2. Анализ требований для выбранных функций Для двух тригонометрических функций и двух обратных к ним — тангенса, косинуса, арктангенса и арккосинуса — анализ требований для дальнейшего построения тестового набора был проведен по описанной выше методике. При этом пришлось иметь дело со следующими их особенностями. Нули, полюса и прообразы специальных значений. Для определения интервалов сохранения знака важно вычислить нули и полюса функции. Для определения интервалов монотонности ту же роль играют максимумы и минимумы. Нули косинуса совпадают с полюсами тангенса и являются числами вида π/2+π·n. Максимумы и минимумы косинуса имеют вид 2π·n и (2n+1)·π. Кроме того, для проверки корректности вычисления тригонометрических функций можно использовать широко известные соотношения вида cos(±π/3+2π·n) = 1/2, cos(±5π/3+2π·n) = -1/2, tg(π/4+π·n) = 1 и tg(-π/4+π·n) = -1.
Во всех этих случаях фигурируют целые кратные иррациональных чисел π/3 или π/4. Чтобы определить накладываемые этими соотношениями ограничения на реализации тригонометрических 167
168
Асимптотики. Тангенс и арктангенс имеют хорошо известные асимптотики в нуле: tg(x) ~ x и arctg(x) ~ x при x ~ 0. Арктангенс имеет горизонтальные асимптоты на бесконечности: arctg(x) ~ π/2 при x ~ +∞ и arctg(x) ~ π/2 при x ~ -∞. Каждая из таких асимптотик определяет некоторые интервалы чисел с плавающей точкой, в которых она должна быть выполнена точно при режиме округления к ближайшему и либо точно, либо же с отступом на одно значение при других режимах («точное» равенство π/2 означает равенство числу с плавающей точкой, к которому π/2 округляется при заданном режиме).
Кроме того, поскольку в окрестности нуля числа с плавающей точкой расположены плотнее, чем в окрестностях других точек, любая функция, имеющая ненулевое значение в 0, должна принимать его же и в некоторой окрестности нуля. Это верно для косинуса и арккосинуса — первый в нуле имеет значение 1, второй — π/2. Границы соответствующих интервалов можно вычислить с помощью метода последовательных приближений. Например, для тангенса имеем tg(1.00100101000010111111111000011011000010000010111101002·2-26 = x1) = 1.0010010100001011111111100001101100001000001011110100 015001…2·2-26; tg(1.00100101000010111111111000011011000010000010111101012·2-26) = 1.0010010100001011111111100001101100001000001011110101 105210…2·2-26; tg(1.01110001001101110100010010010001001000111110111101012·2-26 = x2) = 1.0111000100110111010001001001000100100011111011110101 150011…2·2-26; tg(1.01110001001101110100010010010001001000111110111101102·2-26) = 1.0111000100110111010001001001000100100011111011110111 055100…2·2-26. Следующие из этих соотношений требования к значениям тангенса в окрестности 0 для всех режимов округления сформулированы в Таблице 1. В этой таблице функция next(x) для числа с плавающей точкой x обозначает непосредственно следующее за x число с плавающей точкой, а функция previous(x) — число, предшествующее x. Округление
к0
к -∞
к ближайшему
к +∞
Интервал/точка x = -next(x2)
Previous
previous(x)
(previous(x)) -next(x1) x -x2
previous(x)
0 > x -x1
previous (x)
x
previous(x) x
x=0
0
x1 x > 0 x2 x next(x1) x = next(x2)
x
x x
next(x) next(x)
next(x)
Next (next(x))
Таблица 1. Требования к поведению тангенса в окрестности 0. Аналогичные требования определяются и для других функций в тех интервалах, где их асимптотики превращаются в точные или почти точные равенства. 169
Симметрии. Тангенс и арктангенс являются нечетными функциями, а косинус — четной. Эти свойства учитываются при выборе тестовых значений и проверяются в тестах. Другие симметрии, в частности, периодичность, выполнены для чисел с плавающей точкой только приближенно и не накладывают строгих требований на реализации выбранных функций.
3.3. Выбор тестовых значений В качестве тестовых значений для всех функций выбирались специфические числа с плавающей точкой: 0, -0, +∞, -∞, NaN, минимальное и максимальное положительные денормализованные и нормализованные числа, противоположные им, а также вычисленные границы интервалов действия асимптотик для различных режимов округления. Были выбраны периоды тангенса и косинуса, содержащие хотя бы одну из упоминавшихся выше точек, близких к кратным π (включая 0), π/2, π/3 (для косинуса) и π/4 (для тангенса). Для каждого из выбранных периодов вычислены его левый и правый концы, а также точки, ближайшие на этом интервале к кратным π/2, π/3 (для косинуса) и π/4 (для тангенса). Эти точки разбивают каждый период на несколько интервалов. Всего таких точек меньше, чем 3000, поэтому получающийся набор тестовых данных будет одновременно достаточно представительным и обозримым. Полученные таким образом интервалы можно разбить далее, пользуясь техникой, описанной в статье [1] в разделе 4.2. А именно, разбивать каждый интервал на N более мелких интервалов, при этом получая (N+1) точку. Далее брать все числа, лежащие в рассматриваемом интервале и отстоящие не более чем на K чисел с плавающей точкой. Кроме того, к тестовым значениям добавлены числа, для которых вычисление корректно округленного значения функций наиболее трудоемко. Вот пример такого числа из [21] для косинуса. cos(1.10000000000000000000000000000000000000000000000010012·2-23) = 1.1111111111111111111111111111111111111111111101110000 089110…2·2-01 Для четных или нечетных функций для каждого полученного уже тестового значения в тестовый набор было добавлено противоположное значение. Значение косинуса для него должно быть таким же, а значение тангенса и котангенса вычисляются по-разному в зависимости от режима округления. При округлении к 0 или к ближайшему достаточно просто поменять знак значения нечетной функции, чтобы получить ее значение на противоположном аргументе. При режимах округления к ±∞ нужно менять и знак, и режим — значение нечетной функции при режиме округления к +∞ на противоположном аргументе получается сменой знака из ее значения на исходном аргументе при режиме округления к -∞, и наоборот. 170
4. Результаты тестирования
Косинус
Тестирование проводилось в рамках проекта OLVER с использованием технологии UniTESK. Были протестированы реализации 4-х тригонометрических функций (tan, cos, arctg, arcos) при четырех режимах округления (“к ближайшему”, “к +∞”, “к -∞” и “к нулю”) на следующих пяти платформах: 1. Glibc 2.3.4 (Red Hat 4.64 AMD64); 2. Glibc 2.3.6 (Debian 4 IA32); 3. Glibc 2.4 (SUSE 10.0 IA32); 4. Glibc 2.4 (SUSE 10.0 Itanium); 5. MS Visual C++ RunTime Library (Windows XP IA32).
4.2. Классификация ошибок Для анализа ошибок числа с плавающей точкой были разделены на 5 классов: NaN, ±∞, ±0, нормализованные числа и денормализованные числа. При сборе статистики использовалась следующая классификация ошибок.
“Грубые ошибки” фиксировались, если полученный и правильный результаты лежали в разных классах по приведенной классификации. “Нарушение знака” — полученный результат имеет неправильный знак. “Очень неточный результат” — все 53 бита полученного результата отличаются от соответствующих бит в правильном результате. “Не очень точный результат” — количество неправильных бит в полученном результате меньше 53.
Glibс 2.3.6 (IA32) Glibс 2.4 (IA32) MSVCRT(Windows) Glibc 2.3.4 (AMD64) Glibc 2.4 (Itanium) Glibс 2.3.6 (IA32) Glibс 2.4 (IA32) MSVСRT(Windows) Glibc 2.3.4 (AMD64) Glibc 2.4 (Itanium)
5772 5772 5708 3015 3015
11336 11336 11340 3926 3934
1006 1006 1006 14431 14423
0 0 0 0 0
3914 3914 0 3968 3968
1792 1792 0 1752 1752
6324 6324 5 6310 6310
4010 4010 16035 4010 4010
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
1116 1116 1116 1116 1116
Грубых ошибок ни на одной из платформ зафиксировано не было. Арккосинус во всех протестированных реализациях вычисляется достаточно точно, поскольку наиболее прост для вычисления из рассматриваемых функций. 60 50 40 30 20
В Таблице 2 приведена общая статистика ошибок, обнаруженных при выполнении построенных тестов при режиме округления к ближайшему. Функция/ Грубые НаруОчень Не очень Абсолютно Платформа ошибки шение неточный точный точный знака результат результат результат Тангенс 9305 9305 9337 5010 5010
2691 2691 2700 1445 1445
Таблица 2. Общая статистика обнаруженных ошибок.
4.3. Анализ результатов тестирования
0 0 0 0 0
10128 10128 10115 5359 5359
Арккосинус
Если полученный результат совпадал с правильным (совпадал знак, экспонента и все биты мантиссы), то никакой ошибки не фиксировалось, и результат считался “абсолютно точным”.
Glibс 2.3.6 (IA32) Glibс 2.4 (IA32) MSVCRT(Windows) Glibc 2.3.4 (AMD64) Glibc 2.4 (Itanium)
0 0 0 0 0
Арктангенс
Средняя ошибка
4.1. Тестовые платформы
Glibс 2.3.6 (IA32) Glibс 2.4 (IA32) MSVCRT(Windows) Glibc 2.3.4 (AMD64) Glibc 2.4 (Itanium)
7324 7324 7356 1829 1832
10 0 0
599 599 599 13146 13143
100
200
300
400 500 600 экспонента аргумента
Рис. 1. Средняя величина ошибки (в количестве неправильных знаков результата) при вычислении тангенса в зависимости от экспоненты аргумента (IA32). 171
172
Средняя ошибка
Тангенс и косинус вычисляются с заметным количеством ошибок, в основном связанных с корректной редукцией аргумента — аккуратным приведением аргумента в интервал от 0 до π/2 [19]. Дело в том, что для правильного приведения в этот интервал больших по абсолютной величине значений, необходимо хранить много дополнительных знаков числа π, что, повидимому, не делается ни в одной из проверенных реализаций. Величина ошибок начинает возрастать при некотором удалении от нуля (там, где используемого представления π становится недостаточно для корректной редукции аргумента), причем на 64-разрядных платформах это происходит гораздо позже, чем на 32-разрядных (см. Рис. 1 и 2).
Средняя ошибка
60 50 40 30 20
60
10
50
0 0
200
400
600
40
800 1000 экспонента аргумента
Рис. 3. Средняя величина ошибки (в количестве неправильных знаков результата) при вычислении косинуса в зависимости от экспоненты аргумента (AMD64, режим округления к 0).
30 20
600
10
N 500
0 0
100
200
300
400 500 600 экспонента аргумента
400
Рис. 2. Средняя величина ошибки (в количестве неправильных знаков результата) при вычислении тангенса в зависимости от экспоненты аргумента (AMD64).
300
200
На 32-битных платформах тангенс вычисляется неправильно для чисел, больших 264, на 64-битных платформах соответствующая граница расположена около 2500. Для тангенса картина, показанная на Рис. 1 и 2, примерно сохраняется при всех режимах округления. Для косинуса она такая же при режиме округления к ближайшему, но изменяется при других режимах (см. Ри). Здесь видно влияние других факторов, не связанных с редукцией аргумента.
100
0 0
10 Все точки
20 cos ~1
cos ~0
30 cos ~1/2
40
50
кол-во неправильных бит
Рис. 4. Распределение величины ошибки для косинуса в точках разных типов (IA32). Другая особенность ошибок, обнаруженных при тестировании реализаций тангенса и косинуса на 32-битных платформах, связана с некорректным
173
174
вычислением значений тангенса в точках, близких к его полюсам или нулям, отличным от 0, а также косинуса в точках, близких к его нулям. В обоих случаях ошибки практически для всех таких точек превышают 35-40 бит (т.е. результаты имеют столько неправильных знаков) — см. Рис. 4. Это также, скорее всего, связано с проблемой корректной редукции аргумента, поскольку для правильного вычисления функций в окрестностях их нулей и полюсов редукция аргумента должна быть более точной. Однако для 64-битных платформ этой особенности нет — во всех таких точках тангенс и косинус на них вычисляются правильно. 600
N 500 400 300 200
Использованный метод продемонстрировал хорошие результаты, позволив при помощи небольшого числа тестов обнаружить серьезные ошибки в широко используемых реализациях тригонометрических функций. Большинство найденных ошибок связано с проблемой корректной редукции аргумента тригонометрических функций [19]. Обнаруженные в реализации арктангенса в библиотеке Glibc серьезные ошибки имеют непонятную природу, поскольку возникают в окрестности нуля или на очень больших числах, где арктангенс имеет достаточно хорошие асимптотики, позволяющие вычислять его точно без больших усилий. Вместе с тем для более серьезной проверки корректности вычисления математических функций необходимо расширить набор тестовых точек, где корректное округление результата функции требует повышенной точности вычислений. В работах группы Arienaire [16,21] для каждой функции приводится лишь несколько таких точек, вычисленных с помощью оптимизированных переборных алгоритмов. Для пополнения тестов такими значениями на систематической основе необходимы техники, которые позволят вычислять их гораздо эффективнее. Литература
100 0 0
10 Все точки
20 x ~inf
30 x ~0
x ~1
40
50
кол-во неправильных бит
Рис. 5. Распределение величины ошибки для арктангенса (Glibc). Самая большая неожиданность — значительные ошибки при вычислении арктангенса в реализации библиотеки Glibc на всех платформах. Реализация этой функции от Microsoft работает значительно более корректно — обнаружено лишь 5 небольших ошибок. Полное недоумение вызывают как ошибки на больших по абсолютной величине аргументах (превосходящих 230), где график арктангенса превращается в горизонтальные прямые, так и ошибки, связанные с нарушением знака результата (знак значения арктангенса всегда должен совпадать со знаком аргумента). Рис. 5 показывает распределение величины ошибки вычисления арктангенса в библиотеке Glibc в зависимости от типа тестовых значений.
5. Заключение В данной статье представлены результаты работы по созданию тестового набора на основе метода, предложенного в [1] для тестирования реализаций 4-х математических функций — тангенса, арктангенса, косинуса и арккосинуса. 175
[1] В. Кулямин. Формальные подходы к тестированию математических функций. Труды ИСП РАН, 10:69–114, 2006. [2] IEEE 754-1985. IEEE Standard for Binary Floating-Point Arithmetic. NY: IEEE, 1985. [3] IEEE 854-1987. IEEE Standard for Radix-Independent Floating-Point Arithmetic. NY: IEEE, 1987. [4] ISO/IEC 9899:1999. Programming Languages — C. Geneve: ISO, 1999. [5] IEEE 1003.1-2004. Information Technology — Portable Operating System Interface (POSIX). NY: IEEE, 2004. [6] G. Hanrot, V. Lefevre, J.-M. Muller, N. Revol, and P. Zimmermann. Some Notes for a Proposal for Elementary Function Implementation in Floating-Point Arithmetic. Proc. of Workshop IEEE 754R and Arithmetic Standardization, in ARITH-15, June 2001. [7] D. Defour, G. Hanrot, V. Lefevre, J.-M. Muller, N. Revol, and P. Zimmermann. Proposal for a standardization of mathematical function implementation in floating-point arithmetic. Numerical Algorithms, 37(1–4):367–375, December 2004. [8] http://www.inria.fr/recherche/equipes/arenaire.en.html [9] ISO/IEC 10967-1:1994. Information Technology — Language Independent Arithmetic — Part 1: Integer and Floating Point Arithmetic. Geneve: ISO, 1994. [10] ISO/IEC 10967-2:2002. Information Technology — Language Independent Arithmetic — Part 2: Elementary Numerical Functions. Geneve: ISO, 2002. [11] ISO/IEC 10967-3. Information Technology — Language Independent Arithmetic — Part 3: Complex Integer and Floating Arithmetic and Complex Elementary Numerical Functions. Draft. Geneve: ISO, 2002. [12] http://www.math.utah.edu/~beebe/software/ieee/ [13] http://www.math.utah.edu/pub/elefunt/ [14] http://www.netlib.org/fp/ucbtest.tgz
176
[15] Z. A. Liu. Berkeley Elementary Function Test Suite. M.S. thesis, Computer Science Division, Dept. of Electrical Engineering and Computer Science, University of California at Berkeley, December 1987. [16] D. Stehle, V. Lefevre, P. Zimmermann. Searching Worst Cases of a One-Variable Function Using Lattice Reduction. IEEE Transactions on Computers, 54(3):340–346, March 2005. [17] http://www.loria.fr/~zimmerma/mpcheck/ [18] W. Kahan. Minimizing q*m–n. 1983. Неопубликованные заметки, доступны по http://http.cs.berkeley.edu/~wkahan/testpi/nearpi.c. [19] K. C. Ng. Arguments Reduction for Huge Arguments: Good to the Last Bit. 1992. Доступна как http://www.validlab.com/arg.pdf. [20] А. Я. Хинчин. Цепные дроби. М: Наука, 1978. [21] V. Lefevre, J.-M. Muller. Worst Cases for Correct Rounding of the Elementary Functions in Double Precision. Proc. of 15-th IEEE Symposium on Computer Arithmetic, Vail, Colorado, USA, June 2001.
177