Программирование в DELPHI.

Часть 5. Тестирование и отладка.

 

• Силовая отладка.

   

 

    Под силовой отладкой (brute-force debugging), отладкой "в лоб", понимаются методы отладки, основанные не на возможностях отладчиков, а на трюках, родословная которых, пожалуй, восходит к временам Атанасова и Лебедева, создававших первые ЭВМ по обе стороны океана.
    При разработке программ часто нет необходимости в полной отладке, просто хочется убедиться в том, что какая-либо функция работает так, а не иначе (я весьма часто попадаю в подобные ситуации, когда использую малознакомые функции API или плохо или вовсе недокументированные методы объектов, и мне надо провести эксперимент, чтобы выяснить, так ли я представляю работу функции).
    В этих случаях проще забыть об отладчике и просто добавить пару строк кода для вывода информации. Для этого есть много путей, и о некоторых из них будет рассказано ниже.

 

Вывод отладочной информации в форме.
    Один из способов вывода такой информации — ее вывод непосредственно в форме. Обычно проще всего создать компонент TLabel или подобный ему для непосредственного вывода информации. В таком случае выведенная информация не потеряется даже при перерисовке формы.
    Посмотрите на описания функций ExtractFileDir и ExtractFilePath в справочной системе Delphi 4. Я не берусь точно судить по документации о различии между этими функциями, но я знаю, что мне делать. Я создаю новое приложение (выбрав пункт меню File/New Application) и помещаю в главную форму элемент TButton и два элемента TLabel (форма будет выглядеть так, как на рис. 2.20).
    Дважды щелкните на кнопке TButton и добавьте код к обработчику события OnClick.

    procedure TFormI.ButtonlClick(Sender: TObject);
    begin

        Labell.Caption:= ExtractFileDir(Application.ExeName);

        Label2.Caption:= ExtractFilePath(Application.ExeName);

    end;

    (Application. ExeName возвращает полное имя файла приложения). Нажмите клавишу <F9> для компиляции и запуска приложения и щелкните на кнопке. Теперь вам должно быть ясно, чем различаются эти две функции.
Недавно у меня возникла проблема с чужой DLL, исходного кода которой я, естественно, не имел. Странным было то, что эта DLL выцелела при загрузке и не освобождала большой фрагмент виртуальной памяти. Я создал маленькое приложение, в котором после каждого щелчка на кнопке сообщалось, сколько виртуальной памяти свободно. Мне хотелось сохранять предыдущие результаты, а потому, я использовал элемент управления TMemo и добавлял в него новые строки с результатами.
     Чтобы посмотреть, как это делается, создадим новое приложение и разместим в форме элементы управления TMemo и TButton (и не забудем установить значение свойства TMemo.ScrollBars равным ssVertical). Ваша форма будет выглядеть так, как на рис. 2.21.

 
 
 
Рис. 2.20. Вывод отладочной информации с использованием элемента управления TLabel.
 
Рис. 2.21. Вывод отладочной информации в элемент TMemo

    В обработчик события OnClick добавьте следующий код.

    procedure TFormI.ButtonlClick(Sender: TObject);
    var

        MemStat: TMemoryStatus;

    begin

        VirtualAlloc(nil, 1000000, MEM_RESERVE, PAGE_READWRITE);// 1

        MemStat.dwLength:= SizeOf(TMemoryStatus);              // 2

        GlobalMemoryStatus(MemStat);                            // 3

        Memol.Lines.Add(IntToStr(MemStat.dwAvailVirtual));      // 4

    end;

Не беспокойтесь о деталях вызова API-функции VirtualAlloc в строке 1. Здесь ее вызов требует от операционной системы зарезервировать миллион байтов памяти для дальнейшего использования. API-функция GlobalMemoryStatus возвращает информацию об использовании памяти приложением и системой в целом. Информация возвращается в переменной MemStat, представляющей собой запись типа TMemoryStatus. Перед вызовом GlobalMemoryStatus вы передаете системе информацию о размере структуры, как в строке 2, а затем вызываете функцию (строка 3) и выводите информацию в TMemo в строке 4.
    Скомпилируйте и запустите программу, щелкните несколько раз на кнопке - и увидите, что виртуальная память уменьшается примерно на один мегабайт при каждом щелчке, как и ожидалось. На рис. 2.23 показана форма после нескольких щелчков на кнопке.
    Используя этот метод (без вызова VirtualAlloc), я выяснил, что на самом деле DLL затребовала около 60 Мбайт (!) виртуальной памяти при загрузке и не освободила ее. Даже притом, что Windows 95 предоставляет каждому приложению двухгигабайтовое адресное пространство, потерю 60 Мбайт сложно проигнорировать...
 

Рис. 2.22. Вывод в элемент TMemo информации о количестве доступной виртуальной памяти.

 

ShowMessage
    Кроме вывода информации в форму, можно воспользоваться модальным диалоговым окном. Принципиальное отличие этого метода, в первую очередь, состоит в том, что модальное диалоговое окно останавливает выполнение программы, пока вы его не закроете. Таким образом, у вас имеется достаточно времени, чтобы прочесть и осмыслить полученную информацию.
    Процедура ShowMessage (из модуля Dialogs) идеально подходит для этой цели Она позволяет вывести строку любой длины в простом модальном диалоговом окне. Вам только следует создать строку для вывода и передать ее процедуре (можно также использовать MessageDIg, но в нем слишком много шашечек и бантиков, которые требуют немалых усилий для достижения того же эффекта).
    ShowMessage получает в качестве параметра одну строку, для создания которой я предпочитаю использовать функцию Format, она идеально подходит для этого, будучи одновременно простым и мощным инструментом в умелых руках.
    Рассмотрим простой пример. Используем этот метод для вывода информации, получаемой от уже использовавшейся функции GlobalMemoryStatus.
    Создадим новое приложение и поместим TButton в основную форму. Обработчик события OnClick будет выглядеть следующим образом.

    procedure TFormI.ButtonlClick(Sender: TObject);
    var MemStat: TMemoryStatus;

    begin

        MemStat.dwLength:= SizeOf(TMemoryStatus);

        GlobalMemoryStatus(MemStat);

        with MemStat do ShowMessage(Format('Memory load: %d%%'#13 +

            'Total physical: %d'#13+'Available physical: %d'#13 +

            'Total page file: %d'#13 + 'Available page file: %d'ftl3 +

            'Total virtual: %d'#13 + 'Available virtual: %d',

            [dwMemoryLoad, dwTotalPhys, dwAvailPhys, dwTotalPageFile,

            dwAvailPageFile, dwTotalVirtual, dwAvailVirtual]));

    end;

    Заметьте, что я внес в строку несколько символов #13 (ASCII-символ возврата каретки). Это позволяет разбить строку при выводе на несколько строк, что существенно облегчает чтение информации. На рис 2.23 показано, что получится после запуска программы и щелчка на кнопке.
    Судя по результатам Memory load и Available physical, представленным на рисунке, мне стоит всерьез подумать о наращивании памяти своего компьютера.
 

Рис 2.23 Использование функции ShowMessage для вывода отладочной информации.

 

Вывод на консоль
    Еще один способ вывода отладочной информации— вывод на консоль с использованием процедур Write и WriteLn. Вы можете конвертировать проект в консольное приложение, например, выбрав соответствующую опцию (команду Project/Options, вкладку Linker и опцию Generate Console Application) или поместив директиву $APPTYPE CONSOLE в главный DPR-файл. Учитывая, что ваше приложение— не консольное, воспользуйтесь возможностями условной компиляции и используйте директиву $APPTYPE как показано ниже:

    {$ifdef Debug}
    {$APPTYPE CONSOLE}

    {$endif}

    Теперь вывод на консоль будет осуществляться только в отладочной версии вашего приложения.
    Если вы попытались использовать функцию Write или WriteLn и получили сообщение об ошибке I/O Еггог,  значит, вы забыли сделать проект консольным приложением.
    Обратите внимание, что здесь применяется тот же код, что и раньше, но теперь мы используем вывод на консоль вместо ShowMessage. Убедитесь, что вы создаете консольное приложение, и измените обработчик так, как показано ниже.

    procedure TFormI.ButtonlClick(Sender: T0bject);
    var MemStat: TMemoryStatus;

    begin

        MemStat.dwLength:= SizeOf(TMemoryStatus);

        GlobalMemoryStatus(MemStat);

        with MemStat do

        begin

            WriteLn(Format('Memory load: %d%%',[dwMemoryLoad]));

            WriteLn(Format('Total physical: %d',[dwTotalPhys]));

            WriteLn(Format('Available physical: %d',[dwAvailPhys]));

            WriteLn(Format('Total page file: %d',[dwTotalPageFile]));

            WriteLn(Format('Available page file: %d',[dwAvailPageFile]));

            WriteLn(Format('Total virtual: %d',[dwTotalVirtual]));

            WriteLn(Format('Available virtual: %d',[dwAvailVirtual]));

        end;

    end;

Результат показан на рис. 2.24.

Рис. 2.24. Использование консоли для вывода отладочной информации.

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

 

Запись в Log-файл
    Запись отладочной информации в файл протокола (Log-файл) существенно отличается от предыдущих приемов записи, так как это уже нельзя назвать "быстро и грязно". Это отличная технология, которую можно использовать в любом приложении.
    Запись в файл протокола выполняется так же, как и вывод на консоль, но вместо WriteLn (. . . ) используется WriteLn (LogFile, . . . ), где LogFile — имя файловой переменной типа TextFile. Надо также не забывать открывать этот файл в начале работы приложения и закрывать — в конце. Проще всего этого добиться, поместив соответствующий код в свой модуль, который благодаря возможности условной компиляции подключается только в отладочной версии вашей программы.

Листинг 2.1. Модуль протоколирования отладочной информации.

    unit uLoq;
    interface

    procedure Log(S: Strings-implementation uses

        Windows, SysUtils;

    var

        LogFile: TextFile;

        LogCriticalSection: TRtlCriticalSection;

    procedure Log(S: String);

    var

        SystemTime: TSystemTime;

        FileTime: TFileTime;

    begin

        GetSystemTime (SystemTime) ;

        SystemTimeToFileTime(SystemTime, FileTime) ;

        EnterCriticalSection(LogCriticalSection);

        WriteLn(LogFile, Format('%s %.8x%.8x %5',

            [FormatDateTime('yy.mm.dd hh.inm.ss'. Now),

            FileTime.dwHighDateTime, FileTime.dwLowDateTime, S])) ;

        LeaveCriticalSection(LogCriticalSection) ;

    end;

    procedure Startup;

    var

        FileName: String;

    begin

        InitializeCriticalSection(LogCriticalSection);

        FileName := Format("Log file for %s at %s.txf,

            [ParamStr(O), DateTimeToStr(Now)]) ;

        while Pos(':', FileName) 0 do

            FileName[Pos(':', FileName)] := '.';

        while Pos('/', FileName) 0 do

            FileName[Pos('/', FileName)] := '-';

        while Pos('\', FileName) 0 do

            FileName[Pos('\', FileName)] := '.';

        AssignFile(LogFile, FileName);

        Rewrite(LogFile) ;

    end;

    procedure Shutdown;

    begin

        CloseFile(LogFile) ;

        DeleteCriticalSection(LogCriticalSection) ;

    end;

    initialization Startup;

    finalization Shutdown;

end.

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

    unit MyUnit;
    interface
    uses
        ($ifdef Debug} uLog, {$endif)
        Windows, Messages, SysUtils, Classes,
        . . .

    Затем используйте его приблизительно так.

    {$ifdef Debug)
    Log(Format('Entering the Foo procedure; Bar = %d',[Bar]));

    {$endif}

    He забывайте размещать вызов между директивами условной компиляции, иначе при компиляции коммерческой версии возникнет ошибка.
    Модуль uLog обладает двумя интересными и полезными свойствами. Во-первых, каждая запись в файл предваряется информацией о дате, времени и шестнадцатеричным числом, соответствующим системному времени в миллисекундах. Эта информация может быть весьма полезной, особенно когда вы хотите отследить последовательность событий в приложении. Во-вторых, модуль использует критические разделы (critical section), что обеспечивает доступ к файлу только одной подзадачи в один момент времени.
    На рис. 2.25 показан типичный файл протокола в программе Notepad.

 
 
Рис. 2.25. Пример отладочного файла протокола

    Как правильно использовать файл протокола? Какую информацию в него записывать? Сколько программистов, столько и ответов на эти вопросы.