Unit-тест для Coredump в Windows

Как мне подсказали в комментариях к посту про создание 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-тестировании, тест получился больше, чем сам тестируемый код. Но повторюсь — это того стоит. Буквально скоро расскажу о моих приключениях с модулем таймера, и как меня выручили тесты.

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


Оригинальный пост | Disclaimer

Комментарии