Отладка приложений

         

Блочное тестирование


Я всегда думал, что Энди Гроув (Andy Grove) из Intel имел право назвать свою книгу Only the Paranoid Survive (Выживают только параноики). Это особенно справедливо для инженеров-программистов. Объединяя чьи-нибудь программные модули со своими, проверяйте все чужие данные до последнего бита. На самом деле надо испытывать здоровый скептицизм даже по отношению к самому себе. Утверждения, трассировка и комментарии — вот с чего следует начинать проверку коллег-разработчиков, когда их программы обращаются к вашему модулю. Блочное тестирование — это средство самопроверки, с помощью которого я сам проверяю себя.

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

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

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

Дополнительной выгодой от тестирования проекта является то, что появляется возможность быстро находить и решать проблемы, что, в свою очередь, делает код повторно-используемым и расширяемым. Поскольку возможность многократного использования — священна для программирования, то любые усилия, потраченные на это, оправданы. Мне удалось удачно решить эту проблему при разработке обработчика аварийных сбоев, описанного в главе 9. Тестируя блоки в Windows 98, я заметил, что API-функция syminitiaiize из символьной машины DBGHELP.DLL перестала автоматически загружать символы всех модулей в процессах, как она это делала в Windows 2000. Предположив, что для решения этой задачи в Windows 98 нужна совсем другая утилита, я разработал функцию Bsusyminitialize. С ее помощью я выполнил все блочное тестирование программы обработчика аварийных сбоев, завершив тем самым эту разработку универсальным решением проблемы многократного использования.

Во время кодирования следует постоянно выполнять блочные тесты. По моим представлениям, в отдельном функциональном блоке содержится приблизительно 50 строк кода. Каждый раз, добавляя или изменяя свойство, я повторно запускаю блочный тест, чтобы видеть, не испортил ли я что-нибудь. Неожиданности следует сводить к минимуму. Настоятельно рекомендую выполнять блочные тесты до регистрации кода в главных источниках (master sources). Некоторые организации используют специальные тесты, называемые регистрационными тестами, которые нужно выполнять перед регистрацией кода. Такие тесты значительно уменьшают число аварий при построениях программ и smoke-тестах.

Ключом к большинству эффективных блочных тестов является понятие покрытия (охвата) кода (code coverage).


Даже если вы не усвоите ничего в данной главе, кроме этого понятия, буду рассматривать это как успех. Покрытие кода — это просто процентная доля реально выполняемых операторов в вашем модуле. Если, например, в модуле закодировано 100 выполняемых операторов, а реально после запуска программы выполняется только 85 из них, то речь идет о 85%-ном покрытии кода. При этом предполагается, что любой, невыполненный оператор является потенциальным источником сбоя.

Статистику покрытия кода можно получить двумя способами. Первый способ довольно трудоемок. Он предполагает использование отладчика и установку точек прерывания на каждом исполняемом операторе тестируемого модуля. Когда во время отладочного прогона модуль выполнит очередной оператор, очистите его точку прерывания. Продолжайте выполнение программы до тех пор, пока не будут очищены все точки прерываний (в этом случае покрытие кода считается 100%). Легче подсчитывать процент покрытия при помощи специальных программных инструментов независимых поставщиков, например, TrueCoverage от NuMega или Visual PureCoverage фирмы Rational.



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

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

Следуя рекомендациям этой главы, можно получить в конце разработки довольно эффективные блочные тесты, но работа на этом не заканчивается. Мои блочные тесты можно найти в каталоге ..\SourceCode\BugslayerUtil \Tests на сопровождающем компакт-диске. Я храню их как часть свой кодовой базы, так что коллеги могут легко их отыскать. Когда я вношу изменения в эту базу, то могу легко проверить, не нарушил ли я что-нибудь. Настоятельно рекомендую всем разработчикам установить свои тесты в систему управления версией. Хотя большинство блочных тестов самодокументировано, удостоверьтесь, что вы комментируете все ключевые предположения так, чтобы другие могли быстрее ознакомиться с вашими тестами.



Как и что следует проверять с помощью утверждений


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

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

Правила использования утверждений

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

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

// неправильным?

BOOL GetPathltem ( int i, LPTSTR szltem, int iLen)

{

ASSERT ( ( i > 0 ) &&

( NULL != szltem ) && 

( ( iLen > 0) && ( iLen < MAX_PATH) ) && 

( FALSE = IsBadWriteStringPtr ( szltem, iLen)));

}

// Подходящий способ. Каждый параметр проверяется

// индивидуально, так что вы можете видеть, какой из них неправильный.

BOOL GetPathltem ( int i, LPTSTR szltem, int iLen)

{

ASSERT ( i > 0);

ASSERT ( NULL != szltem);

ASSERT ( ( iLen > 0) && ( iLen < MAX_PATH));


ASSERT ( FALSE == IsBadWriteStringPtr ( szltem, iLen));

}

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

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

// Пример проверки только части ошибочного условия

BOOL EnumerateListltems ( PFNELCALLBACK pfnCallback)

{

ASSERT ( NULL != pfnCallback);

}

Для полной проверки корректности указателя, в операторе ASSERT можно также использовать API-функцию isBadCodePtr:

// Пример полной проверки ошибочного условия

BOOL EnumerateListltems (PFNELCALLBACK pfnCallback)

{

ASSERT ( FALSE = IsBadCodePtr ( pfnCallback));

}

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

 Пример неправильного определения аргумента оператора утверждения: 

 отрицательное значение nCount не будет обнаружено функцией утверждения.

 Function UpdateListEntries(ByVal nCount As Integer) as Integer

Debug.Assert nCount

.

.

.

End Function

' Здесь аргумент вызова определен так (nCount>0) , что Assert-функция 

' правильно отреагирует на отрицательное значение nCount 

Function UpdateListEntries(ByVal nCount As Integer) as Integer

 Debug.Assert nCount > 0

.

.

.

End Function

В первом примере, по существу, проверяются все значения nCount, не равные нулю, так что неправильные значения параметра (nCount <= 0) не будут обнаружены функцией Assert. Во втором условие проверки сформулировано точнее (nCount > 0), так что утверждение, во-первых, оказывается самодокументированным (т.


к. в выражении nCount > 0 явно указано условие проверки) и, во-вторых, функция утверждения должным образом реагирует на все "неправильные" (<= 0) значения параметра nCount.

 Когда функция UpdateListEntries получает в качестве параметра неположительное значение, то функция-утверждение Debug.Assert обнаруживает этот факт при оценке выражения nCount > 0 и выводит на экран панель сообщений ASSERTION FAILURE. — Пер.

В языках С и C++ имеются специальные проверочные функции, приведенные в табл. 3.1 и помогающие создавать достаточно описательные утверждения. Эти функции можно вызывать и из программ на Visual Basic, но делать этого не нужно из-за проблем с указателями.

Таблица 3.1. Вспомогательные функции для создания описательных утверждений в C/C++

Функция

Описание

GetOb j ectType

Функция подсистемы интерфейса графических устройств (GDI), которая возвращает тип GDI дескриптора

IsBadCodePtr

Проверяет правильность указателя памяти

IsBadReadPtr

Проверяет, может ли указатель памяти читать указанное число байт

IsBadStringPt

Проверяет, может ли строчный указатель читать символы до NULL-терминатора (или указанное максимальное число символов)

IsBadWritePtr

Проверяет, может ли указатель памяти записывать указанное число байтов

IsWindow

Проверяет, является ли HWND-параметр правильным окном

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

Visual Basic имеет свой набор функций, помогающих проверять достоверность специфических условий Visual Basic. Все эти функции перечислены в табл 3.2. Если разработчик, следуя общепринятой практике программирования на языке Visual Basic, не использует спецификатор variants и явно определяет спецификаторы ByVal и ByRef для параметров, то ему нет необходимости так часто проверять достоверность типов переменных. Если же вы не придерживаетесь подобной практики, то, по крайней мере, получаете некоторый набор хороших средств для выполнения такой проверки.

Таблица 3.2. Справочные функции для описательных утверждений Visual Basic



Однажды мой друг Фрэнсис Полин



Однажды мой друг Фрэнсис Полин (Francois Poulin), работавший в службе сопровождения, явился на работу с нагрудной табличкой, которая гласила: "Пиши программы так, будто всякий, кто сопровождает твою программу — полный психопат, который знает, где ты живешь!" Фрэнсис никакой не психопат, но он предложил хорошую идею. Разработчик может, конечно, считать, что его программа — идеал ясности и совершенно понятна без всяких комментариев, но на самом деле она столь же плоха для тех, кто ее сопровождает, как сплошные строки команд ассемблера. Вспоминайте табличку Фрэнсиса каждый раз, когда пишете программу.
Работа инженеров-разработчиков преследует две цели: создать решение для пользователя и сделать его удобным для поддержки в будущем. Единственный способ сделать код удобным для поддержки состоит в том, чтобы комментировать его. "Комментировать" не означает просто записывать комментарии, которые разъясняют, что именно делает программа; имеется в виду документирование предположений, подходов к решению задачи и причин для их выбора. Необходимо также отслеживать соответствие комментариев тексту программы.
Рекомендую следующий подход к комментированию:
каждая функция или метод нуждаются в одном-двух предложениях комментариев, которые сообщают: • что подпрограмма делает;
• какие она использует предположения;
• что она ожидает получить через каждый входной параметр;
• что будет содержать каждый выходной параметр при успехе и отказе;
• каждое возможное возвращаемое значение.
 каждая часть функции, которая не полностью очевидна из кода, нуждается в одном или двух предложениях комментариев, которые объясняют, что она делает;  любой интересный алгоритм заслуживает полного описания;  любые нетривиальные ошибки, которые вы исправили в коде, должны быть прокомментированы с указанием номера ошибки и описанием сути исправления;  хорошо расположенные операторы трассировки и утверждений, а также удачные соглашения об именах могут служить отличными комментариями и обеспечивать поддерживающий контекст для кода;  комментируйте так, будто вы собрались сопровождать код в течение пяти лет;  если, просматривая код функции, вы произносите фразы типа: "Это — хакерская штучка" или "Это — действительно хитрый материал", то, вероятно, нужно переписывать функцию, а не комментировать ее. Различие между серьезным, профессиональным разработчиком и тем, кто играет в него, выявляет надлежащая и полная документация в коде. Дональд Кнут (Donald Knuth) однажды заметил, что хорошо написанную программу нужно уметь читать так же, как хорошо написанную книгу. Хотя я не представляю себя лежащим на ковре у камелька с копией исходного кода программы ТеХ, но я полностью согласен с мнением доктора Кнута.
Рекомендую изучить главу 19 "Self-Documenting Code" феноменальной книги Стива МакКоннелла (Steve McConnell) Code Complete (Microsoft Press, 1993). Читая ее, вы увидите, как я учился писать комментарии. Если вы комментируете правильно, то даже если сопровождающий программист окажется психопатом, не сомневайтесь — вы будете в безопасности.

Обзор операторов утверждений для Visual C++ и Visual Basic


В этом разделе приводится краткий обзор и обсуждение различных операторов утверждений, которые используются в языках Visual C++ и Visual Basic. Хотя следует отметить, что вместо них можно создавать и собственные несложные операторы-утверждения, подобные макросу ASSERT, который использован во всех предыдущих примерах.

Макросы assert, _ASSERTw _ASSERTE

Макрос assert исполнительной (run-time) библиотеки языка С определен стандартом ANSI С. Эта версия переносима на все С-компиляторы и платформы и определена во включаемом файле ASSERT.H. При сбое консольных Windows-приложений макрос утверждения assert посылает свое сообщение в стандартный поток вывода ошибок stderr. Если речь идет о Windows-приложении с графическим интерфейсом пользователя (GUI), то при его сбое assert выводит свое сообщение на экран в форме панели сообщений ASSERTION FAILURE... (см. рис. 3.1).

Другой тип операторов утверждений исполнительной С-библиотеки специфичен для Windows. Это макросы _ASSERT и __ASSERTE, которые определены в файле CRTDBG.H. Единственное различие между ними в том, что _ASSERTE выводит в свою выходную панель также и выражение, получаемое через аргумент вызова. Отслеживать это выражение настолько важно, особенно когда программу тестируют специальные инженеры1, что следует всегда использовать именно макрос _ASSERTE, а не _ASSERT. Оба макроса являются частью чрезвычайно полезной отладочной С-библиотеки времени выполнения (подробное описание DCRT-библиотеки приведено в главе 15).

Test engineers — тестирующие инженеры. — Пер. 

История отладочной войны Исчезновение файлов и потоков

 Сражение

При работе с одной из версий программы BoundsChecker фирмы NuMega мы встретились с невероятно трудной проблемой случайных сбоев, которые было почти невозможно дублировать. Единственной зацепкой было то, что дескрипторы файлов и потоков иногда становились неправильными, приводя к беспорядочному закрытию файлов и срыву синхронизации потоков. Разработчиков интерфейса пользователя (U ^-разработчиков) также преследовали случайные сбои, но только при выполнении под отладчиком.
Эти проблемы мучили нас во время разработки и, наконец, настал момент, когда вся команда бросила работу и принялась за исправление этих ошибок.

UI — User Interface, интерфейс пользователя. — Пер.

Подробнее оба отладочных процесса и их терминология поясняются в следующей главе. — Пер.

Результат

Меня буквально смешали с грязью, потому что оказалось, что проблема возникла из-за моей ошибки. В приложении BoundsChecker я отвечал за цикл отладки, и при этом использовалась отладочная библиотека Windows (Windows Debugging API), которая стартует один отладочный процесс (debugger) и управляет другим3 (debuggee), а также отвечает на события отладки, которые генерирует второй процесс. Будучи добросовестным программистом, я видел, что функция WaitForDebugEvent возвращала значения дескриптора некоторых уведомлений о событиях. Например, когда процесс стартовал под отладчиком, тот получал структуру, которая содержала дескриптор процесса и начальный поток для этого процесса.

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

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

Чтобы понять, как эта ситуация привела к проблеме, нужно знать, что когда дескриптор закрывается, операционная система помечает его как "доступный". Операционная система Windows NT 4, которую мы использовали в то время, является особенно агрессивной относительно повторного использования значений дескрипторов (Windows' 2000 демонстрирует такое же агрессивное поведение).


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

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

Урок

Можно было избежать этой проблемы, прочитав упомянутую выше заметку в документации отладочного API. Кроме того (и это— большой урок), я понял, что нужно всегда проверять значения, возвращаемые в cioseHandle. Хотя в такой ситуации мало что можно сделать, но когда вы закрываете неверный дескриптор, то операционная система хотя бы выдает сообщение о соответствующих неполадках, на которое нужно обратить внимание.

Замечу, что если вы пытаетесь повторно закрыть дескриптор или передать некорректное значение в CioseHandle, выполняясь под отладчиком, то Windows NT 4 и Windows 2000 выводят следующее сообщение: "Invalid Handle exception (0x00000008)" (исключение (0x00000008) "Недействительный дескриптор"). Получив такое сообщение, можно остановить выполнение и попытаться выяснить, почему оно появилось.

Хотя макросы assert, _ASSERT и _ASSERTE удобны в работе и бесплатны, они имеют несколько недостатков. С макросом assert связаны две проблемы.

Во-первых, имя файла в его выходном сообщении усекается до 60 символов, так что иногда при завершении программы вы понятия не имеете, какой файл вызвал макрос утверждения. Вторая проблема возникает при работе с проектом, который не использует пользовательского интерфейса, например со службой Windows 2000 или внепроцессным СОМ-сервером.


Когда макрос assert направляет свой вывод в стандартный выходной поток ошибок (stderr), то его можно легко пропустить. А если assert пытается направить свой вывод в панель сообщений, то консольное приложение повиснет, пытаясь закрыть эту панель, т. к. не использует UI-интерфейса и не может выводить на экран никаких окон.

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

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

//Послать сообщение окну. Если время такой посылки истекает (тайм-аут),

// то другой поток зависает, так что нужно прервать данный поток. 

//Напоминаем, что единственный способ проверить, произошел ли сбой 

//функции SendMessageTimeout, состоит в том, чтобы проверить функцию

// GetLastError. Если функция возвратила 0 и последняя ошибка есть 0, 

//то SendMessageTimeout выполнила тайм-аут.

_ASSERTE ( NULL != pDataPacket)

if ( NULL == pDataPacket)

return ( ERR_INVALID_DATA);

}

LRESULT IRes = SendMessageTimeout ( hUIWnd,

WM_USER_NEEDNEXTPACKET,

0

(LPARAM)pDataPacket ,

SMTO_BLOCK ,

10000

&pdwRes ) ;

_ASSERTE ( FALSE != IRes);

if ( 0 == IRes)

{

// Получить значение последней ошибки. 

DWORD dwLastErr = GetLastError ();

 if ( 0 == dwLastErr)

{

// UI висит или нет достаточно быстрой обработки данных.

return ( ERR_UI_IS_HUNG);

}

// Если ошибка в чем-то еще, то существует проблема

//с данными, посылаемыми через параметр.



return ( ERR_INVALID_DATA);

}

return ( ERR_SUCCESS);

.

.

.

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

_ASSERTE ( FALSE != IRes)

(т. е. при вызове макроса с указанным параметром) выводится панель сообщения, а значение последней ошибки становится равным 0, так что в отладочных построениях UI-поток всегда кажется зависшим, тогда как в выпускных построениях бывают ситуации, в которых параметры, пересылаемые в функцию sendMessageTimeout, оказываются некорректными. Кому-то подобная ситуация может и не казаться такой уж проблемной, но мой собственный опыт был иным — с этой ситуацией оказались связанными две ошибки, на отслеживание и исправление которых пришлось потратить много времени. К счастью, представленное ниже утверждение SUPERASSERT позволяет решить эту проблему.

Макросы ASSERT_KINDOFw ASSERT_VALID

Программисты, использующие библиотеку классов MFC, наверняка встречались с двумя дополнительными, специфичными для MFC макросами утверждений, которые чрезвычайно полезны при профилактической отладке. Если классы объявлены с помощью макросов DECLARE_DYNAMIC или DECLARE_SERIAL, то макрос ASSERT_KINDOF позволяет проверить, на что ссылается указатель производного (от cobject) класса — на некоторый конкретный класс или на его производный класс. Макрос ASSERT_KINDOF — это просто оболочка метода cobject: :isKindOf. В следующем фрагменте исходного кода сначала проверяется параметр утверждения ASSERT_KINDOF, а затем выполняется реальная проверка ошибки параметра.

BOOL DoSomeMFCStuffToAFrame ( CWnd * pWnd)

 {

ASSERT ( NULL != pWnd);

ASSERT_KINDOF ( CFrameWnd, pWnd);

if ( (NULL == pWnd) ||

{ FALSE == pWnd->IsKindOf ( RUNTIME_CLASS ( CFrameWnd)))) 

{

return ( FALSE);

.

.

.

// Выполнить некоторую работу для MFC-приложения; Гарантировано,

 // что pWnd будет указывать на класс CFrameWnd или на класс,



 // производньм от CFrameWnd.

.

.

.

Второй MFC-макрос-утверждение — ASSERT_VALID — сводится к вызову функции AfxAssertvaiidObject, которая подтверждает корректность указателя класса, производного от cobject. После подтверждения корректности указателя, макрос ASSERT_VALID вызывает объектный метод Assertvaiid. Для проверки внутренних структур данных в производных классах этот метод можно переопределять. Метод Assertvaiid считается сильным средством для выполнения глубоких проверок, поэтому нужно переопределять его для всех ключевых классов приложения.

Оператор Debug.Assert

С одной стороны, жизнь Visual Basic-программистов намного легче, чем у программистов C/C++, потому что Visual Basic не требует обширных проверок корректности типов параметров и указателей (до тех пор, пока не используются параметры типа variant). Однако, с другой стороны, правильно организовать профилактическое программирование средствами языка Visual Basic довольно трудно. В Visual Basic имеется всего один встроенный оператор утверждения — Debug.Assert, хотя у него и существует целых четыре версии.

Это — хорошие новости. Но есть и плохие: Debug.Assert нельзя использовать, когда вы действительно в нем нуждаетесь, т. е. при отладке компилированного кода. Думаю, что создатели Visual Basic сделали большую ошибку,

не разрешив функции Debug.Assert компилироваться в родной (native) код. Оператор Debug.Assert доступен только при выполнении внутри интегрированной среды разработки (IDE) Visual Basic. Когда функция утверждения терпит неудачу при отладке, то программист попадает в окно IDE на строку Debug.Assert. Хотя Debug.Assert активна только в IDE, желательно использовать ее в максимально возможной степени, чтобы контролировать все проблемы заранее, еще на уровне исходного кода.

Для себя я разрешил все проблемы с Debug.Assert, когда просматривал книгу Advanced Visual Basic 6.0 (2nd ed., Microsoft Press, 1998) компании The Mandelbrot Set, основанной в Англии. Для этой книги Марк Пирс (Mark Реагс) написал замечательную дополнительную программу для Visual Basic, называемую Assertion Sourcerer.


Она одна стоит целой книги (и остальная часть книги тоже превосходна). Эта программа автоматически отслеживает предложения Debug.Assert в исходной программе и помещает после них вызов реального оператора утверждения. Она также вычисляет имя исходного файла и номер строки, в которых была обнаружена проблема. Дополнительно к размещению реальных операторов утверждений в исходном коде, программа Assertion Sourcerer еще и убирает их, когда работа с ними заканчивается!

Программу Марка Пирса можно легко расширить на поиск предложений Debug.Print и вставку после них реальных операторов трассировки. В листинге 3-2 показан исходный код авторского файла VBASSERTANDTRACE.BAS, который содержит реализации всех тех реальных операторов утверждений и трассировки, о которых только что шла речь. Для обработки утверждений в нем используется макрос SUPERASSERT, обсуждению которого посвящен следующий раздел.

Листинг 3-2. Файл VBASSERTANDTRACE.BAS

Attribute VB_Name = "VBAssertAndTrace"

'''''''''''''''''''''''''''''''''''''''''''''

' Copyright (с) 1999-2000 John Robbins — All rights reserved.

' "Debugging Applications" (Microsoft Press) 

'

' Чтобы использовать этот файл: 

' Не обязательно (но настоятельно!) рекомендуется:

' использовать подключаемый Visual Basic-модуль Assertion

' Sourcerer Марка Пирса (Mark Pearce) из

' "Advanced Microsoft Visual Basic 6.0" (2nd ed).

' Он будет отлавливать все предложения Debug.Assert

' программы и помещать под каждым таким предложением

' обращение к BugAssert, так что в компилированный Visual Basic-код

' будут вставлены реальные операторы утверждений.

' В работе с Debug.Assert придерживайтесь следующих правил:

' 1. Компилируйте BUGSLAYERUTIL.DLL, потому что этот файл

' использует несколько экспортированных функций.

' 2. Разместите операторы Debug.Assert в исходном коде программы.

' 3. Когда вы будете готовы компилировать программу, используйте

' подключаемый модуль Марка Пирса, чтобы добавить обращения к



' BugAssert.

' 4. Добавьте данный файл в ваш проект.

' 5. Компилируйте свой проект и понаблюдайте за утверждениями.

' Можно также вызывать различные функции библиотеки

' BUGSLAYERUTIL.DLL, чтобы установить различные опции и выходные

' дескрипторы.

'''''''''''''''''''''''''''''''''''''''''''

Option Explicit

' Объявить все функции BUGSLAYERUTIL.DLL, которые этот модуль

' может вызывать.

Public Declare Sub DiagOutputVB Lib "BugslayerUtil" _

(ByVal sMsg As String)

Public Declare Function DiagAssertVB Lib "BugslayerUtil" _ 

(ByVal dwOverrideOpts As Long, _ 

ByVal bAllowHalts As Long,

 _ ByVal sMsg As String) _

 As Long

Public Declare Function AddDiagAssertModule Lib "BugslayerUtil" _ 

(ByVal hMod As Long) _ 

As Long

Public Declare Function SetDiagAssertFile Lib "BugslayerUtil" _

  (ByVal hFile As Long) _ 

As Long

Public Declare Function SetDiagAssertOptions Lib "BugslayerUtil" _

  (ByVal dwOpts As Long) _ 

As Long

Public Declare Function SetDiagOutputFile Lib "BugslayerUtil" _ 

(ByVal dwOpts As Long) _ 

As Long

Private Declare Function GetModuleFileName Lib "kerne!32" _

 Alias "GetModuleFileNameA" _ 

(ByVal hModule As Long, _ 

ByVal IpFileName As String, _ 

ByVal nSize As Long) _

 As Long Public Declare Sub DebugBreak Lib "kerne!32" ()

' Авторский макрос TRACE. Его можно использовать для вызова любого

' другого макроса. Кроме того, программа Assertion Sourcerer расширена

' для добавления TRACE-операторов (после предложений Debug.Print)

Public Sub TRACE(ByVal sMsg As String)

DiagOutputVB sMsg End Sub

' Функция BugAssert, вставленная с помощью

 ' Assertion Sourcerer

 Public Sub BugAssert(ByVal vntiExpression As Variant, sMsg As String)

CallAssert vntiExpression, 0, sMsg 

End Sub

' Подпрограмма SUPERASSERT.

 Public Sub SUPERASSERT{ByVal vntiExpression As Variant, sMsg As String)



CallAssert vntiExpression, 7, sMsg 

End Sub

Private Sub CallAssert{ByVal vntiExpression As Variant, _

ByVal iOpt As Long,

 _ sMsg As String)

 If (vntiExpression) Then

Exit Sub Else

' Следующий флажок используется, чтобы определить, вызывалась ли

 ' уже функция InDesign. Вызывать эту функцию повторно нет 

' необходимости.

Static bCheckedDesign As Boolean 'False по умолчанию. 

' Флажок, разрешающий остановки, я пересылаю в DiagAssertVB.

 ' Если этот флажок установлен в 1, то DiagAssertVB разрешит 

' останавливать приложение. Если этот флажок установлен в 0, 

' приложение выполняется в VB IDE так, что DiagAssertVB не будет

 ' разрешать остановки. Если пользователь запускается 

' внутри VB IDE, то прерывание довольно опасно и может погубить 

' весь ваш дневной труд! 

Static lAllowHalts As Long 

' Вызвать InDesign только раз.

 If (False = bCheckedDesign) Then 

If (True = InDesign()) Then

lAllowHalts = 0 

Else

lAllowHalts = I 

End If

bCheckedDesign = True

 End If

Dim IRet As Long

IRet = DiagAssertVB(iOpt, lAllowHalts, sMsg)

 If (I = IRet) Then

' Пользователь .хочет прервать выполнение. Однако

 ' прерывание не разрешается, если выполнение 

' происходит внутри VB IDE.

If (1 = lAllowHalts) Then

DebugBreak

 End If

 End If 

End If 

End Sub

'''''''''''''''''''''''''''''''''''''''''

' Эта замечательная функция взята из превосходной главы Пита Морриса "On

' Error GoTo To Hell" (с.25,26 в "Advanced Microsoft Visual Basic 6.0")

' InDesign позволяет проверять, выполняетесь ли вы в VB IDE. Я благодарен

' Питу, разрешившему мне использовать эту функцию!

'''''''''''''''''''''''''''''''''''''''

Public Function InDesign() As Boolean

' Я оставлю только один комментарий Пита — он превосходен.

' Только для этого и нужен Debug.Assert!

Static nCallCount As Integer

Static bRet As Boolean ' По умолчанию этот флажок False.

nCallCount = nCallCount + 1

Select Case nCallCount

Case 1: ' Первый вход (выполнение Debug.Assert)

Debug.Assert InDesign() Case 2: ' Второй вход, когда Debug.Assert уже выполнен

bRet = True

 End Select

' Если был вызван Debug.Assert, возвратить True, 

' чтобы предотвратить ловушку. 

InDesign = bRet

' Переустановить счетчик для будущих вызовов. 

nCallCount = 0

 End Function



Операторы утверждений


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

 Термин assertion (англ.) является логическим утверждением (или суждением), проверяющим некоторые (в данном случае программные) условия на истинность (true) или ложность (false). — Пер.

Если контролируемое утверждением условие ложно (имеет значение false), то говорят, что проверка окончилась неудачей. Утверждения следует использовать в дополнение к нормальной проверке ошибок. Традиционно, утверждения определяются в программе, в виде специальных функций или макросов1, которые выполняются только в отладочных построениях. В случае ложности контролируемого утверждением условия, оператор утверждения посылает на экран монитора сообщение, в котором указывается, проверка какого условия оказалась неуспешной (т. е. в момент проверки условие имело значение false). Я расширяю определение утверждения, включая в него условно компилируемый код, проверяющий условия и предположения, которые являются слишком сложными для обработки в обычном операторе утверждения. Утверждения — ключевой компонент профилактического программирования, потому что они помогают разработчикам и тестирующим инженерам определять не только то, что ошибки присутствуют, но также и почему они происходят.

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

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

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

 Обычно с именами Assert — для функций, или ASSERT — для макросов (см. примеры этого раздела). — Пер



Описание Функции isArray Проверяет


BOOL CheckDriveFreeSpace ( LPCTSTR szDrive) 

{

ULARGE_INTEGER ulgAvail; 

ULARGE_INTEGER ulgNumBytes; 

ULARGE INTEGER ulgFree;

if ( FALSE == GetDiskFreeSpaceEx ( szDrive ,

&ulgAvail ,

 &ulgNumBytes ,

 &ulgFree ))

{

ASSERT ( FALSE);

return ( FALSE); 

}

}

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

Мой друг Дейв Анджел (Dave Angel) указал мне, что в операторе ASSERT языков С и C++ можно использовать логическую операцию NOT (!), а в качестве ее операнда — строку символов. Эта комбинация позволяет выводить более информативные сообщения, из которых, по крайней мере, можно почерпнуть какую-то идею относительно того, в чем же заключалась ошибка (без просмотра исходного кода). Следующий пример показывает надлежащий способ контроля ложного условия. К сожалению, уловка Дейва Анджела не работает в Visual Basic.

// Надлежащее использование утверждения 

BOOL CheckDriveFreeSpace ( LPCTSTR szDrive) 

{

ULARGE_INTEGER ulgAvail; 

ULARGE_INTEGER ulgNuinBytes; 

ULARGE INTEGER ulgFree;

if ( FALSE = GetDiskFreeSpaceEx ( szDrive ,

&ulgAvail ,

 &ulgNumBytes, 

&ulgFree )) 

{

ASSERT ( !"GetDiskFreeSpaceEx failed!");

 return ( FALSE); 

}

}

Можно усовершенствовать прием Дейва, используя для формирования проверочного условия логическую операцию AND (&&). В следующем примере показано, как можно добавить к тексту обычного ASSERT-сообщения дополнительное уточняющее сообщение:

BOOL AddToDataTree ( PTREENODE pNode) 

{

ASSERT ( ( FALSE == IsBadReadPtr 

( pNode, sizeof ( TREENODE))) && 

"Invalid parameter!");


.
.
.
}
Что проверяют операторы утверждений
Теперь посмотрим, что необходимо проверять с помощью операторов утверждений. Судя по предыдущим примерам, прежде всего нужно проверять те данные, которые поступают в функцию через аргументы ее вызовов из других программ. Существует опасность, что таким образом в функцию будут направляться некорректные данные (например, данные некорректных для этой функции типов или значений). Выполняя проверки соответствующих параметров, операторы утверждений значительно облегчают процесс отладки и реализуют идею профилактического программирования.
Ниже показан исходный код одной из ключевых функций (stopDebugging) простого отладчика, описанного в главе 4, в которой для контроля параметра использован макрооператор ASSERT. Обратите внимание, что в теле функции сначала выполняется оператор утверждения (ASSERT) и сразу за ним — обработка реальной ошибки. Напомним, что операторы утверждений лишь контролируют правильность параметров, да и то лишь на этапах отладки, и никоим образом не заменяют нормальной обработки ошибок.
BOOL DEBUGINTERFACE_DLLINTERFACE _stdcall
StopDebugging ( LPHANDLE IpDebugSyncEvents) 
{
ASSERT ( FALSE ==
IsBadWritePtr ( IpDebugSyncEvents,
sizeof ( HANDLE) * NUM_DEBUGEVENTS)); 
if ( TRUE == IsBadWritePtr ( IpDebugSyncEvents,
sizeof ( HANDLE) * NUM_DEBUGEVENTS))
 {
SetLastError ( ERROR_INVALID_PARAMETER);
 return ( FALSE); 
}
// Сигнал потоку отладки с именем события его закрытия. 
VERIFY ( SetEvent ( IpDebugSyncEvents[ CLOSEDEBUGGER ]));
return ( TRUE);
 }
Параметры внутренних private-функций программного модуля не всегда нуждаются в контроле с помощью утверждений. Он необходим только в том случае, если речь идет о внешнем вызове внутренней функции. Более того, если параметр, используемый внешним вызовом, однажды уже прошел через проверку оператором утверждения (на этапе отладки, например), то нет необходимости повторять такие проверки в отлаженном (рабочем) варианте программы.


Это значит, что из отлаженного варианта все операторы утверждений можно спокойно удалить. Однако на этапе отладки иногда полезно использовать сплошные ASSERT-проверки — для всех параметров всех внутренних функций модуля. Возможно, это позволит отловить некоторые внутренние ошибки в модуле.
При выборе параметров для проверок с помощью утверждений полезно придерживаться некоторой средней линии поведения и проверять не все, а лишь наиболее "опасные" для устойчивой работы модуля параметры внутренних вызовов. Правильный отбор таких параметров возможен лишь после приобретения достаточных навыков в разработке программ. Только накопив определенный опыт программирования, вы почувствуете, в каких точках программы можно столкнуться с проблемами, и сможете отобрать внутренние параметры для проверок с помощью утверждений.
Другими объектами ASSERT-контроля являются возвращаемые значения функций. ASSERT-оператор проверяет корректность возвращаемого значения непосредственно перед его возвратом в вызывающую программу. Некоторые разработчики предпочитают проверять с помощью операторов утверждений почти каждое возвращаемое значение. В листинге 3-1 приводится определение функции startDebugging (из отладчика, описанного в главе 4), использующее операторы утверждений для проверки корректности возвращаемых значений. Если в функции вычисляется некорректное значение, то оператор утверждения выводит на экран предупреждающее сообщение.
 Листинг 3-1. Примеры ASSERT-проверок возвращаемых значений ;
HANDLE DEBUGINTERFACE_DLLINTERFACE _stdcall
StartDebugging ( LPCTSTR szDebuggee , 
LPCTSTR szCmdLine , 
LPDWORD IpPID ,
 CDebugBaseUser * pUserClass ,
 LPHANDLE IpDebugSyncEvents ) 
{
// ASSERT-проверки параметров.
ASSERT ( FALSE == IsBadStringPtr ( szDebuggee, MAX__PATH)); 
ASSERT ( FALSE == IsBadStringPtr ( szCmdLine, MAX_PATH)); 
ASSERT ( FALSE == IsBadWritePtr ( IpPID, sizeof ( DWORD))); 
ASSERT ( FALSE == IsBadReadPtr ( pUserClass,


sizeof ( CDebugBaseUser *)));
ASSERT' ( FALSE == IsBadWritePtr ( IpDebugSyncEvents,
sizeof ( HANDLE) *
NUM_DEBUGEVENTS)); 
// Обычные проверки параметров.
if ( ( TRUE == IsBadStringPtr ( szDebuggee, MAX_PATH) ) ||
 ( TRUE •== IsBadStringPtr ( szCmdLine, MAX_PATH) ) ||
( TRUE — IsBadWritePtr ( IpPID, sizeof ( DWORD) )) || 
( TRUE == IsBadReadPtr ( pUserClass,
sizeof ( CDebugBaseUser *))) || 
( TRUE == IsBadWritePtr ( IpDebugSyncEvents,
sizeof ( HANDLE) *
NUM_DEBUGEVENTS) ) )
{
SetLastError ( ERROR_INVALID_PARAMETER);
return ( INVALID_HANDLE_VALUE); 
}
// Обработка начального уведомления о том, что данная
 // функция будет ждать, пока не начнет выполняться отладочный поток HANDLE hStartAck;
// Строка, используемая для начального уведомления TCHAR szStartAck [ МАХ_РАТН ];
 // Загрузить строку начального уведомления, 
if ( 0 == LoadString ( GetDllHandle () ,
IDS_DBGEVENTINIT ,
 szStartAck ,
 sizeof ( szStartAck) ))
 {
ASSERT ( !"LoadString IDS_DBGEVENTINIT failed!");
return ( INVALID_HANDLE_VALUE); 
}
// Создать событие начального уведомления.
 hStartAck = CreateEvent ( NULL , // Безопасность по умолчанию
TRUE , // Событие ручной переустановки FALSE ,
 // Сигнал Initial state = Not szStartAck);
 // Имя события ASSERT ( FALSE != hStartAck);
 if ( FALSE == hStartAck) 
{
TRACE ( "StartDebugging : Unable to create Start Ack event\n");
return ( INVALID_HANDLE_VALUE); 
}
// Связать параметры.
THREADPARAMS StParams; 
stParams.lpPID = IpPID; 
stParams.pUserClass = pUserClass;
 stParams.szDebuggee = szDebuggee;
 stParams.szCmdLine = szCmdLine ;
 // Дескриптор для потока отладки HANDLE hDbgThread; 
// Попытка создать поток.
hDbgThread = (HANDLE)_beginthread ( DebugThread, 0, sstParams); 
ASSERT ( NULL != hDbgThread);
 if ( NULL == hDbgThread)
 {
TRACE ( "StartDebugging : _beginthread failed\n");


VERIFY ( CloseHandle ( hStartAck));
return ( INVALID_HANDLE_VALUE); 
}
// Ждать, пока поток отладки не стабилизируется, и запустить
::WaitForSingleObject ( hStartAck, INFINITE);
// Освободить дескриптор уведомления.
VERIFY ( CloseHandle ( hStartAck));
// Проверить, выполняется ли еще поток отладки. Если нет,
// отладка, вероятно, не может стартовать.
DWORD dwExitCode = ~STILL_ACTIVE;
if ( FALSE == GetExitCodeThread ( hDbgThread, SdwExitCode))
{
ASSERT ( !"GetExitCodeThread failed!");
return ( INVALID_HANDLE_VALUE); 
}
ASSERT ( STILL_ACTIVE = dwExitCode);
 if ( STILL_ACTIVE != dwExitCode)
{
TRACE ( "StartDebugging : GetExitCodeThread failedXn");
return ( INVALID_HANDLE_VALUE); 
}
// Создать события синхронизации, чтобы главный поток мог 
// сообщать циклу отладки, что делать.
 BOOL bCreateDbgSyncEvts =
CreateDebugSyncEvents ( IpDebugSyncEvents, *lpPID);
 ASSERT ( TRUE = bCreateDbgSyncEvts);
 if ( FALSE = bCreateDbgSyncEvts)
{
// Это — серьезная проблема. Есть выполняющийся поток отладки,
//но нет возможности создавать события синхронизации, которые
// нужны потоку пользовательского интерфейса для управления
// потоком отладки. Здесь можно только завершить поток отладки и
// выполнить возврат. Больше сделать ничего нельзя.
TRACE ( "StartDebugging : CreateDebugSyncEvents failedW) ;
VERIFY ( TerminateThread ( hDbgThread, (DWORD)-1));
return ( INVALID_HANDLE_VALUE);
}
return ( hDbgThread); 
}
И, наконец, операторы утверждений используются в том случае, когда возникает необходимость проверить некоторое предположение. Например, если в спецификациях функции говорится о том, что она требует 3 Мбайт дискового пространства, то нужно проверить это предположение с помощью оператора утверждения. Другой пример: если функция получает (через аргумент вызова) массив указателей на определенную структуру данных, то необходимо проверить данные этой структуры и подтвердить правильность каждого индивидуального элемента.


В обоих этих случаях, как и при проверке большинства других предположений, нет возможности проверять предположение с помощью обычных функций или макросов. В этих ситуациях следует использовать технику условной компиляции, которая, как указывалось ранее, должна стать частью комплекта инструментов для проверки утверждений. Поскольку код, который выполняется во время условной компиляции, работает на "живых" данных, нужно предпринять дополнительные меры предосторожности, гарантирующие неизменность состояния программы. В программах на Microsoft Visual C++ и Visual Basic я предпочитаю, если возможно, реализовывать эти типы утверждений в виде отдельных функций. Таким способом можно защитить от изменений любые локальные переменные внутри исходной функции. Кроме того, условно компилированные функции утверждений могут свободно использовать окно Watch (об этом мы поговорим в главе 5, где речь пойдет об отладчике Visual C++). Следующий пример показывает условно компилированную функцию-утверждение ValidatePointerArray, которая выполняет глубокие проверки корректности на массивах данных.
#ifdef _DEBUG
void VaiidatePointerArray ( STDATA * pData, int iCount)
{
// Сначала проверить буфер массива. 
ASSERT ( FALSE == IsBadReadPtr ( pData,
iCount * sizeof ( STDATA *)));
 for ( int i = 0; i < iCount; i++) 
{
ASSERT ( pData[ i ].bFlags < DF_HIGHVAL);
ASSERT { FALSE == IsBadStringPtr ( pDataf i ].pszName,
MAX_PATH));
 }
}
#endif
void PlotDataltems ( STDATA * pData, int iCount)
#ifdef _DEBUG
VaiidatePointerArray ( pData, iCount);
#endif
}
Макрос VERIFY
Прежде чем двигаться дальше, поговорим о макросе VERIFY, который использовался при разработке библиотеки классов Microsoft Foundation Classes MFC). В отладочных построениях этот макрос ведет себя так же, как обычное утверждение: если условие установлено в 0, то VERIFY открывает панель с предупреждающим сообщением. Однако, в отличие от обычного утверждения, в выпускной конфигурации параметр этого макроса остается в исходном коде и считается нормальной частью программной процедуры.


В сущности, VERIFY можно рассматривать как нормальное утверждение с побочными эффектами, которые сохраняются и в выпускных конфигурациях программы. Строго говоря, в утверждениях любого типа нельзя использовать условия, вызывающие какие-либо побочные эффекты. Все-таки в одной ситуации макрос VERIFY полезен: когда имеется функция, возвращающая ошибочное значение, которое нельзя проверить другим способом. Например, если вызывается функция ResetEvent, чтобы очистить дескриптор свободного события, и вызов терпит неудачу, то мало что можно сделать. Вот почему большинство программистов вызывает ResetEvent и никогда не проверяет возвращаемое значение ни в отладочных, ни в выпускных построениях. Если поместить вызов в макрос VERIFY, то, по крайней мере, можно будет получить уведомление во время отладочных построений, что что-то не так. Конечно, можно достичь тех же результатов, используя ASSERT, но VERIFY избавляет от необходимости создавать новую переменную только для того, чтобы сохранять и проверять возвращаемое значение. Такая переменная, вероятно, использовалась бы только в отладочных построениях.
Многие MFC-программисты, вероятно, применяют макрос VERIFY просто по привычке. Однако в большинстве случаев вместо этого нужно проверять возвращаемое значение. Хорошим примером использования VERIFY является метод cstring: :Loadstring, который загружает строки ресурса. Такой способ хорош в отладочных построениях, потому что если Loadstring завершается неудачно, то макрос VERIFY предупреждает об этом. Однако если сбой Loadstring происходит в выпускном построении, то приходится заканчивать работу с неинициализированной переменной. В лучшем случае здесь можно получить незаполненную строку, но, скорее всего, задача будет завершаться аварийно. Мораль этой истории заключается в том, что возвращаемые значения нужно проверять. Если же вы собираетесь использовать макрос VERIFY, то нужно всегда задаваться вопросом, не приведет ли отказ от проверки возвращаемого значения к каким-нибудь проблемам в выпускном построении?


Программа SUPERASSERT


Рассмотрев проблемы, возникающие с системными утверждениями, покажем, как можно усовершенствовать оператор утверждения, чтобы существенно расширить выводимую им информацию о причинах возникновения проблемы. На рис. 3.1 показан пример панели сообщений программы SUPERASSERT. Поля Program, File и Line самоочевидны. Интерес представляют те, что следуют за полем Last Error.

В SUPERASSERT значения последних ошибок переводятся в их текстовые представления. При сбоях функций API просматривать сообщения об ошибках в текстовой форме чрезвычайно полезно, т. к. можно сразу же увидеть, почему соответствующие функции завершились неудачно, и быстрее начинать отладку. Например, если функция GetModuieFileName завершается потому, что размер входного буфера недостаточен, SUPERASSERT установит значение последней ошибки равным 122, что соответствует строчному значению ERROR_INSUFFICIENT_BUFFER (ошибка недостаточного размера буфера) из WINERROR.H. Увидев текст "The data area passed to a system call is too small" (область данных, переданная системному вызову, слишком мала), вы будете точно знать, что это за проблема и как ее следует решать.

Рис. 3.1. Пример панели сообщений программы SUPERASSERT

Кроме того, если вы посмотрите на строку Last Error на рис. 3.1, то увидите, что это не стандартное Windows-сообщение об ошибке. Если вы устанавливаете собственные значения последней ошибки (что я и рекомендую делать), то для трансляции таких сообщений можно добавить в программу SUPERASSERT собственный модуль ресурсов сообщений. Чтобы получить дополнительную информацию об использовании собственных ресурсов сообщений, просмотрите в MSDN тему "Message Compiler" (Компилятор сообщений). Существует и дополнительный стимул для применения таких ресурсов: с их помощью гораздо легче осуществлять интернационализацию приложения.

Чрезвычайно полезна часть панели, расположенная ниже строки Last Error. Это — трасса стека. Она показывает путь к оператору утверждения. SUPERASSERT старается показывать как можно больше информации в сообщениях утверждений, чтобы не нужно было собирать ту же информацию с помощью отладчика.


Вот еще одно интересное свойство SUPERASSERT: можно отказаться от открытия панели его сообщений. Поначалу это может показаться контрпродуктивным, но я ручаюсь, что это не так! Если вы следовали рекомендациям главы 2 и начинали тестирование отладочных конструкций с помощью инструмента регрессивного тестирования (regression-testing tool), то знаете, что управление такими панелями (со случайными сообщениями утверждений) почти невозможно. Из-за подобных проблем инженерам, тестирующим ПО, не очень нравится возиться с отладочными конструкциями. Работая же с программой SUPERASSERT, можно указать, чтобы вывод направлялся в функцию OutputDebugsString, дескриптор файла или и туда, и туда. Такая гибкость позволяет управлять кодом, получать всю обширную информацию утверждений, и иметь возможность автоматизировать отладочные построения. Кроме того, такое утверждение будет работать и в тех случаях, когда приложение не содержит интерфейса пользователя.

Пользоваться программой SUPERASSERT довольно легко. При работе в среде С и C++ для этого нужно только включить файл заголовка BUGSLAYERUTIL.H и установить связь с библиотекой BUGSLAYERUTIL.LIB. В листинге 3-3 показан файл DIAGASSERT.H, который содержит все макросы и функции и автоматически включается в заголовочный файл BUGSLAYERUTIL.H.

Листинг 3-3. DIAGASSERT.H (включенный в BUGSLAYERUTIL.H)

/ - - - - - - - - - - - - - - - - - - - - - -

"Debugging Applications" (Microsoft Press)

Copyright (с) 1999-2000 John Robbins — All Rights Reserved.

/- - - - - - - - - - - - - - - - - - - -

#fndef _DIAGASSERT_H

#define _DIAGASSERT_H

#ifdef _cplusplus

extern "C" {

#endif //_cplusplus

#include <tchar.h>

/////////////////////////////////////////////

            Директивы препроцессора #define

//////////////////////////////////////////////

// Основной материал должен быть доступен как для выпускных, так и для

 // отладочных построений. // Использовать глобальные флажки утверждений



#define DA_USEDEFAULTS OxOOOO

// Включает показ утверждений в панели сообщений (по умолчанию).

#define DA_SHOWMSGBOX OxOOOl

// Включает показ утверждений как через OutputDebugString (по умолчанию).

// the default.

#define DA_SHOWODS 0x0002

// Показывает трассу стека в утверждении. Выключен по умолчанию в

// макросе ASSERT и включен в макросе SUPERASSERT.

ifdefine DA_SHOWSTACKTRACE 0x0004

/*- - - - - - - - - - - - - - - - - - - - - 

ФУНКЦИЯ : SetDiagAssertOptions 

ОПИСАНИЕ :

Устанавливает глобальные режимы для нормального макроса ASSERT.

 ПАРАМЕТРЫ :

dwOpts — флажок новых режимов 

ВОЗВРАЩАЕТ :

Предыдущие режимы

- - - - - - - - - - - - - - - - - - - - - */

DWORD BUGSUTIL_DLLINTERFACE _stdcall

SetDiagAssertOptions ( DWORD dwOpts);

 /*- - - - - - - - - - - - - - - - - - - - - 

ФУНКЦИЯ : SetDiagAssertFile 

ОПИСАНИЕ :

Устанавливает дескриптор файла, в который будут записываться данные    любого утверждения. Чтобы отключить регистрацию, вызывайте эту функцию     с параметром INVALID_HANDLE_VALUE. Набор режимов из SetDiagAssertOptions еще применим; эта функция позволяет регистрировать assertion-информацию в файле.

В дескрипторе файла не делается никаких проверок ошибок

или записей в него.

 ПАРАМЕТРЫ :

hFile — дескриптор файла

 ВОЗВРАЩАЕТ :

Дескриптор предыдущего файла

 - - - - - - - - - - - - - - - - - - - - -* /

HANDLE BUGSUTILJ3LLINTERFACE _stdcall

 SetDiagAssertFile ( HANDLE hFile);

 /* - - - - - - - - - - - - - - - - - - - - - 

ФУНКЦИЯ : AddDiagAssertModule 

DISCUSSION :

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

 выбираться строки ошибок

 ОПИСАНИЕ :

hMod — добавляемый модуль

ВОЗВРАЩАЕТ :

TRUE - модуль был добавлен.

FALSE - внутренняя таблица заполнена.

  - - - - - - - - - - - - - - - - - - - - -*/

BOOL BUGSUTIL_DLLINTERFACE _stdcall

AddDiagAssertModule ( HMODULE hMod);

/*- - - - - - - - - - - - - - - - - - - - 



ФУНКЦИЯ : DiagAssert

 ОПИСАНИЕ :

Функция утверждения для программ на С и C++ 

ПАРАМЕТРЫ :

dwOverrideOpts — DA_* режимы для переопределения глобальных умолчаний для этого вызова в DiagAssert

szMsg — сообщение для показа в панели сообщений

 szFile — файл, который показывается в утверждении

 dwLine — номер строки, в которой имеется утверждение 

ВОЗВРАЩАЕТ :

FALSE — игнорировать утверждение.

TRUE — запустить DebugBreak.

  - - - - - - - - - - - - - - - - - - - - - -* /

BOOL BUGSUTIL_DLLINTERFACE _stdcall

DiagAssertA ( DWORD dwOverrideOpts , 

LPCSTR szMsg , 

LPCSTR szFile DWORD dwLine ); 

BOOL BUGSUTILJDLLINTERFACE _stdcall

DiagAssertW ( DWORD dwOverrideOpts ,

 LPCWSTR szMsg , 

LPCSTR szFile

DWORD dwLine ) ;

#ifdef UNICODE

#define DiagAssert DiagAssertW ttelse

#define DiagAssert DiagAssertA

#endif

/*- - - - - - - - - - - - - - - - - - 

ФУНКЦИЯ : DiagAssertVB 

ОПИСАНИЕ :

Функция утверждения для Visual Basic-программ.

 ПАРАМЕТРЫ

dwOverrideOpts — DA_* режимы для переопределения глобальных умолчаний

для этого вызова в DiagAssert

bAllowHalts — Если TRUE, то не показывает кнопки Retry и Ignore

 szMsg — Выводимое сообщение. За форматирование строки

ответственна сторона Visual Basic

 ВОЗВРАЩАЕТ :

FALSE — игнорировать утверждение.

TRUE - запустит DebugBreak.

 - - - - - - - - - - - - - - - - - - - */

BOOL BUGSUTILJDLLINTERFACE _stdcall

DiagAssertVB ( DWORD dwOverrideOpts,

 BOOL bAllowHalts, 

LPCSTR szMsg);

/*- - - - - - - - - - - - - - - - - - - -

ФУНКЦИЯ : SetDiagOutputFile

 ОПИСАНИЕ :

Устанавливает дескриптор файла, куда будут (по желанию) записаны любые trace-операторы. Чтобы выключить регистрацию, вызовите эту функцию с параметром INVALID_HANDLE_VALUE.

Не делается никаких проверок ошибок для дескриптора файла или каких-либо записей в него. 

ПАРАМЕТРЫ :

hFile — дескриптор файла 

ВОЗВРАЩАЕТ :

Дескриптор предыдущего файла



 - - - - - - - - - - - - - - - - - */

HANDLE BUGSUTIL_DLLINTERFACE _stdcall

SetDiagOutputFile ( HANDLE hFile);

 /*- - - - - - - - - - - - - - - - -

ФУНКЦИЯ : DiagOutput 

ОПИСАНИЕ :

Обеспечивает подпрограмму трассировки для посылки строк через

OutputDebugString

 ПАРАМЕТРЫ :

szFmt — форматная строка

... — параметры, которые будут расширены в szFmt 

ВОЗВРАЩАЕТ :

Нет.

 - - - - - - - - - - - - - - - - - -  */

void BUGSUTIL_DLLINTERFACE

DiagOutputA ( LPCSTR szFtat, ...); 

void BUGSUTIL_DLLINTERFACE

DiagOutputW ( LPCWSTR szFmt, ...);

#ifdef UNICODE

#define DiagOutput DiagOutputW

#else

idefine DiagOutput DiagOutputA

#endif

/*- - - - - - - - - - - - - - - - - - 

ФУНКЦИЯ : DiagOutputVB

 ОПИСАНИЕ :

Обеспечивает подпрограмму трассировки для посылки строк через

OutputDebugString для Visual Basic-программ 

ПАРАМЕТРЫ :

szMsg — строка сообщения 

ВОЗВРАЩАЕТ :

нет.

- - - - - - - - - - - - - - - - - - - - - -*/

void BUGSUTIL_DLLINTERFACE _stdcall

DiagOutputVB ( LPCSTR szMsg); 

/*/////////////////////////////////////

Директивы #undef

////////////////////////////////////////*/ 

#ifdef ASSERT

#undef ASSERT

#endif

 #ifdef assert

#undef assert

#endif 

#ifdef VERIFY

#undef VERIFY 

#endif

#ifdef TRACE 

3undef TRACE

 #endif

#ifdef TRACED 

#undef TRACED

 #endif

 #ifdef TRACE1

#undef TRACE1

#endif

 #ifdef TRACE2

#undef TRACE2

 #endif

#ifdef TRACE3

 #undef TRACE3 

#endif

/*////////////////////////////////////

_DEBUG определен

///////////////////////////////////////*/

#ifdef _DEBUG

/*//////////////////////////////////////////////

Директивы #define

/////////////////////////////////////////////*/

// Различные глобальные режимы, которые могут быть установлены

// в SetDiagAssertOptions. Если любой из этих режимов пересылается

//в DiagAssert в первом параметре, то это значение будет переопределять



// глобальные установки.

// Макрос assert используется ASSERT и SUPERASSERT.

// Выключить "conditional expression is constant" ("условное выражение

// является константой") из-за того, что while(0).

// Нужно сделать это выключение глобально, потому что при расширении

// макроса происходит ошибка компиляции.

#pragma warning ( disable : 4127)

#ifdef PORTABLE_BUGSLAYERUTIL

#define ASSERTMACRO(a,x)                         \

do                                            \

{                                               \

if ( !(x)                                       &&\

DiagAssert ( a, _T ( #x), _FILE_, _LINE_) )     \

{                                            \

DebugBreak () ;                                \

}                                             \



} while (0)

#else //!PORTABLE_BUGSLAYERUTIL

#define ASSERTMACRO(a,x)                       \

do                                             \

{                                             \

if ( !(x)                                       &&\

DiagAssert ( a, _T ( Ix), _FILE_, _LINE_) . )   \

{                                       \ 

_asm int 3                                    \

}                                            \

} while (0)

#endif // PORTABLE_BUGSLAYERUTIL

 // Нормальное утверждение. Оно использует умолчания модуля.

#define ASSERT(x) ASSERTMACRO(DA_OSEDEFAULTS, x)

 // Выполнить assert.

#define assert ASSERT // Доверяй, но проверяй!



#define VERIFY(x) ASSERT(x)

// Полный assert со всеми украшениями

#define SUPERASSERT(x) ASSERTMACRO ( DA_SHOWSTACKTRACE | \

DA_SHOWMSGBOX | \

 DA_SHOWODS , \

 x ,)

// Макрос режимов

#define SETDIAGASSERTOPTIONS(x) SetDiagAssertOptions(x) 

// Добавить макрос модуля

#define ADDDIAGASSERTMODULE(x) AddDiagAssertModule(x)

 // Макрос трассировки TRACE

#ifdef _cplusplus

#define TRACE ::DiagOutput

#endif

#define TRACED(sz)              DiagOutput(_T("Is"), _T(sz))

#define TRACEl(sz, pi)          DiagOutput(_T(sz), pi)

 #define TRACE2(sz, pi, p2)     DiagOutput(_T(sz), pi, p2)

 #define TRACE3(sz, pi, p2, p3) DiagOutput(_T(sz), pi, p2, p3)

#else // !_DEBUG 

/*/////////////////////////////////////////

_DEBUG !!HE!! определен

//////////////////////////////////////////*/

 #define ASSERTMACRO(a,x)

#define ASSERT(x)

#define VERIFY(x) ((void)(x))

#define SUPERASSERT(x)

#define SETDIAGASSERTOPTIONS(x)

#define ADDDIAGASSERTMODULE(x)

#ifdef _cplusplus

//inline void TraceOutput(LPCTSTR, ...) { }

#define TRACE (void)0

#endif

#define TRACED(fmt)

#define TRACE1(fmt,argl)

 #define TRACE2(fmt,argl,arg2)

 #define TRACE3(fmt,argl,arg2,arg3)

 #endif // _DEBUG

#ifdef _cplusplus

}

#endif //_cplusplus

#endif // _DIAGASSERT_H

С помощью программы SUPERASSERT можно автоматически переадресовывать все вызовы ASSERT и assert к своим функциям. Макросы _ASSERT и __ASSERTE не переадресовываются, чтобы не мешать другим работать с отладочной библиотекой времени выполнения. Не затрагиваются также макросы Visual Basic ASSERT_KINDOF и ASSERT_VALID. Для программ на Visual Basic нужно только включать в проект файл VBASSERTANDTRACE.BAS.

Используя макрос или функцию SUPERASSERT, вы автоматически получите трассу стека. Для макроса ASSERT трассировка стека по умолчанию выключена, т.


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

Общий вопрос отладки

Почему константы всегда помещаются в левой части условных операций?

При просмотре моего кода можно заметить, что я всегда использую операторы вида:

if ( INVALID_HANDLE_VALUE == hFile)

вместо

if ( hFile == INVALID_ HANDLE_VALUE)

Этот стиль используется для того, чтобы избежать ошибок. В первой версии можно легко пропустить один из знаков равенства, что приведет к ошибке во время компиляции. Вторая же версия может и не выдавать предупреждения (что зависит от его уровня), но будет изменять значение переменной. Как в C/C++, так и в Visual Basic, при попытке назначать значение константе будет выдаваться ошибка компилятора. Если когда-либо вам приходилось прослеживать ошибку, включающую случайное назначение, то вы знаете, насколько труден поиск ошибок такого типа.

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

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



В этой главе представлены лучшие



В этой главе представлены лучшие методы профилактического программирования, которые можно использовать для отладки на этапе кодирования. Наилучшая методика включает повсеместное использование утверждений,
чтобы получать управление всякий раз, когда возникает проблема. Представленный здесь код SUPERASSERT позволяет устранять все проблемы, связанные с утверждениями, поставляемыми в компиляторах Microsoft. Поддержка и отладка кода намного облегчается, если в дополнение к утверждениям используется трассировка и комментарии. Наконец, наиболее важные эталоны качества для инженеров — блочные тесты. Если код должным образом протестирован перед его регистрацией, то это позволяет устранить многие ошибки и проблемы, которые позже могут помешать работе обслуживающих инженеров.
Единственный способ правильного блочного тестирования состоит в том, чтобы во время тестирования выполнять специальную инструментальную программу, которая вычисляет покрытия кода. Нужно стремиться достигать, по крайней мере, 85—90%-ных значений покрытия кода прежде, чем вы зарегистрируете его в главных источниках. Чем больше времени потрачено на отладку кода во время разработки, тем меньше времени позже уйдет на его отладку.

Трассировка


Утверждения, возможно, наилучший прием профилактического программирования, но операторы трассировки, если их правильно использовать совместно с утверждениями, на самом деле позволяют отлаживать приложения без отладчика. Подобно утверждениям, макросы TRACE (для С и С ++) и Debug.Print (для Visual Basic) применяются и в выпускных компиляциях, причем их можно использовать в исходном тексте столько раз, сколько нужно. Некоторые программисты воспринимают операторы трассировки как средства отладки priritf-стиля. Не стоит недооценивать мощь этой методики, потому что большинство приложений было отлажено прежде, чем были изобретены диалоговые отладчики.

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

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

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

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

Планируя стратегию трассировки, вся команда тратит некоторое время на обдумывание методики форматирования соответствующих операторов. Если все члены команды используют аналогичный формат, то поиск информации с помощью §гер1-утилит или написания простых синтаксических анализаторов для регистрационных журналов — довольно простая задача. Я предпочитаю использовать формат "функция: оператор трассировки". Начиная формат с имени функции, можно легко отыскивать только те функции, которые нужно видеть. Будьте осторожны, чтобы не слишком усложнять этот формат. Если он будет слишком сложным, то разработчики не смогут его запомнить и поэтому не будут использовать.

Обычно операторы трассировки видны только в окне Output отладчика. Однако свободно распространяемая на www.sysinternals.com утилита DebugView/Enterprise Edition Марка Руссиновича (Mark Russinovich) позволяет видеть операторы трассировки даже тогда, когда приложение выполняется вне отладчика. Я всегда запускаю эту программу. Просматривая операторы трассировки, можно видеть, что происходит в приложениях. Утилита DebugView/Enterprise Edition особенно полезна при работе с мультипроцессными СОМ-приложениями, потому что можно видеть все межпроцессные взаимодействия в одном месте. Другая область, в которой эта утилита оказывает неоценимую помощь, — это СОМ-приложения, выполняющиеся в контекстах, которыми разработчик не может управлять — такие, например, как Microsoft Internet Information Services (US).

 Речь идет о программах поиска строк в текстовых файлах. Название происходит от соответствующей команды UNIX (grep). В UNIX используется также поисковая команда find, которая ищет файлы в каталогах. — Пер.