*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***
Показаны сообщения с ярлыком ошибки. Показать все сообщения
Показаны сообщения с ярлыком ошибки. Показать все сообщения

четверг, 26 февраля 2009 г.

Unit-тест для Coredump

Как мне подсказали в комментариях к посту про создание coredump файлов в Windows, если изменить код возврата из функции обработки исключения, то можно подавить появление стандартного окна об ошибке. Посему родился unit-тест для этого модуля.

Модифицированный текст файла coredump.cpp, в котором с помощью макроса UNIT_TESTING встроена поддержка для тестирования. Если этот макрос определен, то, как я уже сказал, подавляется появление окна с ошибкой, и coredump файл создается с постоянным именем.

Файл coredump.cpp:
#include <windows.h>
#include <dbghelp.h>
#include <stdio.h> // _snprintf

// Наш обработчик непойманного исключения.
static LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo);

// Статический экземпляр переменной, конструктор которой
// вызывается до начала функции main().
static struct CoredumpInitializer {
CoredumpInitializer() {
SetUnhandledExceptionFilter(&ExceptionFilter);
}
} coredumpInitializer;

LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo) {
char fname[_MAX_PATH];

SYSTEMTIME st;
GetLocalTime(&st);

HANDLE proc = GetCurrentProcess();

#ifdef UNIT_TESTING
lstrcpy(fname, "___coredump.dmp");
#else
// Формируем имя для coredump'а.
_snprintf(
fname, _MAX_PATH,
"coredump-%ld-%ld-%04d%02d%02d%02d%02d%02d%03d.dmp",
GetProcessId(proc), GetCurrentThreadId(),
st.wYear, st.wMonth, st.wDay,
st.wHour, st.wMinute, st.wSecond, st.wMilliseconds
);
#endif

// Открываем файл.
HANDLE file = CreateFile(
fname,
GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL
);

MINIDUMP_EXCEPTION_INFORMATION info;
info.ExceptionPointers = ExceptionInfo;
info.ThreadId = GetCurrentThreadId();
info.ClientPointers = NULL;

// Собственно, сбрасываем образ памяти в файл.
MiniDumpWriteDump(
proc,
GetProcessId(proc),
file,
MiniDumpWithFullMemory,
ExceptionInfo ? &info : NULL,
NULL, NULL
);

CloseHandle(file);

#ifdef UNIT_TESTING
return EXCEPTION_EXECUTE_HANDLER;
#else
return EXCEPTION_CONTINUE_SEARCH;
#endif
}
Теперь, собственно, тест (файл coredump_unittest.cpp):
#include "gtest/gtest.h"

#include <fstream>
#include <windows.h>
#include <stdlib.h>

TEST(Coredump, CoredumpCreation) {
const char* coredump = "___coredump.dmp";

// На всякий случай заведомо стираем старые файлы.
EXPECT_EQ(0, std::system("cmd.exe /c del ___coredump_main.* 1>nul 2>&1"));

// Создаем файл с тестовой программой.
std::string program = "int main() { *(char *)0 = 0; return 0; }";
std::ofstream os("___coredump_main.cpp");
os << program << std::endl;
os.close();

// Компилируем тестовую программу с опцией UNIT_TESTING.
// С этой опцией coredump файл будет создаваться с постоянным
// именем "___coredump.dmp", и будет подавляется окно с сообщением
// об ошибке.
EXPECT_EQ(
0, std::system(
"cl /Zi /DUNIT_TESTING /Fe___coredump_main.exe"
" ___coredump_main.cpp coredump.cpp dbghelp.lib"
" 1>nul 2>&1"
)
);

// На всякий случая удаляем старый coredump файл.
std::remove(coredump);

// Убеждаемся, что файл действительно удалился.
std::ifstream isdel(coredump);
EXPECT_FALSE(isdel.good());
isdel.close();

// Запускаем тестовую программу.
ASSERT_EQ(0xC0000005, std::system("___coredump_main.exe"));

// Проверяем, создался ли файл coredump.dmp.
std::ifstream is(coredump);
EXPECT_TRUE(is.good());
is.close();

// Удаляем за собой временные файлы.
EXPECT_EQ(0, std::system("cmd.exe /c del ___coredump_main.* 1>nul 2>&1"));
std::remove(coredump);
}
Данный тест имеет ряд существенных недостатков. Во-первых, он использует файловую систему, и во-вторых, он вызывает компилятор, что занимает небольшое, но все же время. Недостатки неприятные, но в целом приемлемые.
Кстати, Google Test Framework умеет делать так называемые "смертельные" (death) тесты. То есть можно протестировать именно аварийное "падение" фрагмента кода, например, из-за нарушения защиты памяти, и для проведения такого теста не надо вручную компилировать что-либо, как мы делали тут. К сожалению, эта возможность основана на использования юниксового системного вызова fork() и поэтому доступна только на UNIX платформах.
Дежурный файл для запуска тестов runner.cpp:
#include "gtest/gtest.h"
int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Традиционно, для компиляции тестов нам нужна Google Test Framework. Как я уже писал, вы можете скачать мою модификацию этой библиотеки, которая сокращена без потери какой-либо функциональности до двух необходимых файлов gtest/gtest.h и gtest-all.cc.
Компилируем, например в Visual Studio 2008:
cl /EHsc /I. /Fecoredump_unittest_vs2008.exe /DWIN32 runner.cpp coredump_unittest.cpp gtest\gtest-all.cc
Запускаем:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from Coredump
[ RUN ] Coredump.CoredumpCreation
[ OK ] Coredump.CoredumpCreation
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran.
[ PASSED ] 1 test.
Работает.

Сразу скажу, я проверял все это только под Windows XP SP2 и Server 2003. Пожалуйста, сообщайте, если есть какие-то проблемы или тонкости под другими виндами.

Как это часто бывает в unit-тестировании, тест получился больше, чем сам тестируемый код. Но повторюсь — это того стоит. Буквально скоро расскажу о моих приключениях с модулем таймера, и как меня выручили тесты.


Другие посты по теме:

четверг, 12 февраля 2009 г.

Универсальная информация об ошибках в Windows и UNIX

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

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

Итак, привожу ниже класс, который я использую для получения информации об ошибке, произошедшей в операционной системе. Можно узнать код ошибки и его текстовое объяснение, если оно предоставляется. Это не бог весть какой сложный и оригинальный класс, но у меня он работает без каких-либо "допиливаний" на Windows 32- и 64-бит, Linux 2.6 32- 64-бит SPARC и Intel, Sun OS 5.10 SPARC и Intel, AIX, HP-UX и HP-UX IA64. К тому же, этот класс безопасен для мультипотокового использования (что лично для меня, например, очень важно).

Итак, класс SystemMessage. Все члены статические, так что можно работать с ними без создания экземпляра класса.
Пространство имен, как обычно, ext, так что измените, если необходимо.
Файл systemmessage.h:
#ifndef _EXT_SYSTEM_MESSAGE_H
#define _EXT_SYSTEM_MESSAGE_H

#include <string>

namespace ext {

class SystemMessage {
public:
// Эта функция возращает код ошибки.
static int code();
// Эта функция по коду ошибки возвращает ее текстовое описание, если
// таковое предоставляется операционной системой. Если нет, то
// возвращается строка "?".
static std::string message(int code);
};

} // namespace ext

#endif // _EXT_SYSTEM_MESSAGE_H
Файл systemmessage.cpp
#include "SystemMessage.h"

#ifdef WIN32
#include <windows.h>
#else
#include <string.h>
#include <unistd.h>
#include <errno.h>
#endif

namespace ext {

int SystemMessage::code() {
#ifdef WIN32
return GetLastError();
#else
return errno;
#endif
}

// Если система по какой-то причине не имеет функции strerror_r,
// то придется лазить напрямую в таблицу сообщений об ошибках.
// Для этого надо при компиляции определить макрос LIBC_NO_STRERROR_R.
// Пока я видел такое только на HP-UX IA64 v2.
#ifndef WIN32
#ifndef LIBC_NO_STRERROR_R
extern "C" int sys_nerr;
extern "C" char* sys_errlist[];
#endif
#endif

std::string SystemMessage::message(int code) {
char msg[1024];

#ifdef WIN32

// Версия для Windows
FormatMessage(
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
code,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
msg,
sizeof(msg) - 1,
NULL
);

char* p = msg + strlen(msg);

// Обрезаем c конца '\r', '\n' и '.'
for(p = msg + strlen(msg) - 1;
p >= msg && (*p == '\n' || *p == '\r' || *p == '.'); --p)
*p = 0;

#elif LIBC_NO_STRERROR_R

// Если UNIX-платформа не имеет функции strerror_r, то делаем ее
// работу вручную. Пока я встретил такое только на HP-UX IA64 v2.
if (code < 0 || code >= sys_nerr)
return "?";

strncpy(msg, sys_errlist[code], sizeof(msg) - 1);

// Если сообщение об ошибке длинее чем sizeof(msg)-1, то '\0'
// не будет скопирован, поэтому добавляем его вручну.
msg[sizeof(msg) - 1] = 0;

#else

// Для нормальной UNIX-системы просто вызываем strerror_r.
if (strerror_r(code, msg, sizeof(msg) - 1) < 0)
return "?";

#endif

// Поможем компилятору по возможности оптимизировать
// возвращаемое значение как rvalue.
return std::string(msg);
}

} // namespace ext
Теперь посмострим это в работе.
Я как-то пока не придумал, как универсально написать unit-тест для этого класса, так как предсказуемые результаты будут все равно различны для каждой платформы. А писать тесты под все платформы как-то топорно. Хочется гармонии, а тут пока ее нет. Если кто имеет идею, как универсально тестировать этот класс на всех платформах — поделитесь, пожалуйста.
Тестовая программа systemmessage_test.cpp:
#include <iostream>
#include <fstream>

#include "systemmessage.h"

int main(int argc, char* argv[]) {
// Пытаемся открыть заведомо несуществующий файл.
std::ifstream is("__non_existing_file__");

// Печатаем ошибку.
int error = ext::SystemMessage::code();
std::cout
<< error << ", "
<< ext::SystemMessage::message(error)
<< std::endl;

return 0;
}
Компилируем в Visual Studio:
cl /EHsc /Fesystemmessage_test_vs2008.exe /DWIN32 systemmessage_test.cpp systemmessage.cpp
Запускаем systemmessage_test_vs2008.exe:
2, The system cannot find the file specified
Получили примерно ожидаемое виндовое сообщение об ошибке.

Теперь компилируем в Cygwin:
g++ -o systemmessage_test_cygwin.exe systemmessage_test.cpp systemmessage.cpp
Запускаем systemmessage_test_cygwin.exe:
2, No such file or directory
Получили сообщение об ошибке в стиле UNIX.
Повторюсь, в данном классе нет ничего удивительного и сложного. Просто это весьма универсальный и переносимый исходник.
И небольшая ремарка. В мире UNIX существует два диалекта функции strerror_r: XSI-версия (когда определен макрос _XOPEN_SOURCE, и он равен 600) и GNU-версия (доступная в libc, начиная с версии 2.0). Разница в том, что первая (XSI-версия) просто кладет сообщение об ошибке в предоставленный буфер и также возвращает код успешности или неуспешности своей работы в виде int'а. Нормальный UNIX-подход. Вторая версия (GNU) возвращает не int, а, собственно, указатель на строку с ошибкой, причем указываеть он может как на предоставленный функции буфер, так и куда-то еще, например, на какой-то внутренний буфер. Данный класс рассчитан на работу с XSI-версией функции strerror_r. Поэтому, если вдруг при компиляции этого класс на UNIX-системах вы получите сообщение об ошибке в использовании этой функции, то определите макрос _XOPEN_SOURCE в значение 600 (-D_XOPEN_SOURCE=600 для компилятора), тем самым будет принудительно использоваться XSI-версия этой болезной функции.