суббота, 29 августа 2009 г.

Тестирование с помощью CxxTest

Я вообще не отношу себя к адептам Test Driven Development, но считаю тесты довольно важной и удобной штукой. Правда дальше самопальных тестов к отдельным функциям и классам, выполняемых по #ifdef-у или в отдельном консольном проект дело не шло. Но ведь надо пользоваться чем-нибудь общепринятым, да и вообще уделять тестам больше внимания. Подумав так, решил встроить-таки хотя бы несколько тестов в свой дипломный проект.
В прошлый раз, когда я пытался приучить себя к тестам, меня отпугнула замороченость CppUnit (клона jUnit для C++), поэтому в этот раз я начал с обзора других средств для тестирования кода на C++.
Вообще я немного поковырял googletest, UnitTest++, и даже Boost Test Library, но речь сейчас не о них.
В конце концов, я искал простое и функциональное решение, безо всяких дурацких регистраций тестов и прочей ненужной синтаксической ерунды.
В конце концов я выбрал CxxTest.

CxxTest


Основные преимущества:
  • Не требует RTTI и шаблонов.
  • Не требует механизма исключений для работы (но может их отлавливать при необходимости).
  • Не зависит и не требует никаких внешних библиотек.
  • Не требует вручную регистрировать тесты (вот оно!).
  • Распространяется как набор заголовочных файлов и исходного кода (ну ещё препроцессор отдельно), не требуется сборка никаких библиотек.
Итак, как же с ним работать.
  1. Тесты организуются в наборы (suite). Каждый набор представляется классом, наследуемым от CxxTest::TestSuite. Тестами в наборе считаются все функции, имя которых начинается с "test" (в любом регистре, напимер test1, Test_Function).
  2. Если нужно делать инициализацию/деиницилизацию уровня набора, то в нём объявляются статические функции createSuite и destroySuite.
  3. Созданный заголовочный файл отдаётся генератору на Perl (cxxtestgen.pl) или Python (cxxtestgen.py), который генерирует исходный файл с точкой входа. В этот файл включается созданный заголовочный файл с тестами
  4. cxxtestgen.py --error-printer -o runner.cpp MyTestSuite.h
  5. Этот исходный файл потом собирается как угодно в приложение, которое и является тестом.
  6. Для тестов есть много разнообразных макросов с говорящими названиями: TS_ASSERT, TS_ASSERT_EQUALS, TS_ASSERT_DELTA или даже TS_ASSERT_THROWS_ANYTHING.
Итак, заголовочный файл может выглядеть примерно таким образом:
include <cxxtest h>

class MyTestSuite : public CxxTest::TestSuite
{
public:
void testAddition( void )
{
TS_ASSERT( 1 + 1 > 1 );
TS_ASSERT_EQUALS( 1 + 1, 2 );
}

void testMultiplication( void )
{
TS_ASSERT_EQUALS( 2 * 2, 5 );
}
};

Запустив этот "тест", можно увидеть:
# ./test
Running 2 tests.
test.h:15: Expected (2 * 2 == 5), found (4 != 5)
Failed 1 of 2 tests
Success rate: 50%

Кроме того, если передать генератору параметры --gui=Win32Gui или --gui=QtGui, то программа соберётся со страшным графическим интерфейсам из одного прогресс-бара. Мне подобное не нужно, но кому-то это может показаться удобно, например если тестов в файле много и они выполняются очень долго.

Интеграция с CMake


Так как я использую CMake в качестве системы сборки, то прикручивать тесты буду именно к ней.
В CMake нашелся модуль для "поиска" и использования CxxTest. Но модуль какой-то невнятный и неудобный — искать не умеет, исполняемые файлы создаёт только из получившегося после препроцессора исходного файла, то есть никаких других файлов ни прилинковать, никаких других исходников не добавить. Если тесты тянут по зависимостям функции из других файлов это недопустимо.
В итоге я немного переделал код, найденный в wiki сайта CxxTest и получил такой макрос:

SET(CXXTEST_EXECUTABLE ${PROJECT_SOURCE_DIR}/3rdparty/cxxtest/cxxtestgen.pl)

MACRO(unit_test NAME CXX_FILE FILES)
SET(PATH_FILES "")
# Мне это не нужно, но если файлы расположены в
# другом каталоге то может понадобиться
#FOREACH(part ${FILES})
# SET(PATH_FILES "${CMAKE_CURRENT_SOURCE_DIR}/${part}" ${PATH_FILES})
#ENDFOREACH(part ${FILES})
SET(PATH_FILES ${FILES})

SET(CXX_FILE_REAL "${CMAKE_CURRENT_SOURCE_DIR}/${CXX_FILE}")
SET(CXXTEST_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${NAME}.cxx")
ADD_CUSTOM_COMMAND(
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${NAME}.cxx"
COMMAND ${CXXTEST_EXECUTABLE} --error-printer -o "${CXXTEST_OUTPUT}" ${CXX_FILE_REAL}
DEPENDS "${FILE}")
SET_SOURCE_FILES_PROPERTIES(${CXXTEST_OUTPUT} PROPERTIES GENERATED true)

ADD_EXECUTABLE("${NAME}" "${CXXTEST_OUTPUT}" ${PATH_FILES})
TARGET_LINK_LIBRARIES("${NAME}" ${CXXTEST_LINK_LIBS})
ADD_TEST("${NAME}" "${EXECUTABLE_OUTPUT_PATH}/${NAME}")
ENDMACRO(unit_test)

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

Интеграция с CTest


Команда ADD_TEST в макросе добавляет тест для CTest.
Чтобы CTest вообще работал, перед применением его команд в CMake надо вызвать ENABLE_TESTING().
Ну и для каждого заголовочного файла с тестом вызывать созданный макрос unit_test.
После сборки проекта, можно запустить тесты несколькими способами.
  1. make test — выполняет все тесты, выводит о них Passed или Failed. Это можно вставить в скрипты сборки и не вспоминать о них пока тесты не поломаются.
  2. Непосредственный запуск CTest: ctest, более подробный ctest -v или даже ctest -vv.
  3. Если CMake создаёт проект для Visual Studio, то создаётся отдельный проект для тестов, который можно запустить.
Всё хорошо, всё работает, осталось только приучить себя писать тесты...
Дополнительные материалы:

  1. Руководство CMake

  2. Руководство CTest

  3. Руководство CxxTest

четверг, 13 августа 2009 г.

Пустые деструкторы и auto_ptr

Думал некоторое время над довольно простой вещью.
Допустим, у нас есть некоторый класс, который объявлен заранее и используется каким-нибудь "умным" указателем:

class B; // forward declare

class A
{
public:
A();
private:
auto_ptr<B> b;
};

Такое описание мы включаем в header, в cpp соответственно включаем и header, где описан B. Всё правильно, для создания указателя компилятору не нужно видеть описание класса.
Проблема в том, что я не объявил у класса A деструктор. Так как "умные" указатели вызывают для указателя delete в своём деструкторе, нас ждёт проблема: при вызове delete компилятор должен видеть определение класса, чтобы вызвать ему деструктор или перегруженный оператор delete (если определения класса нет - получаем Undefined Behaviour, хотя например g++ показывает warning). Соответственно, если в одном из исходников, куда мы включаем свой header, включён файл с определением B, а в другой - нет, то мы прямо-таки нарываемся на нарушение ODR:
  • Если бы в классе A был объявлен деструктор, то инстанцирование деструктора auto_ptr<B> было бы произведено там, а там у нас есть определение B.
  • Если же деструктора у A нет, то создаётся деструктор по умолчанию, который вызывает деструктор auto_ptr<B>. Но создаётся он во всех файлах, куда включён header с A. Какую потом реализацию выберет компоновщик неизвестно.


Таким образом, опять приходим к прописным истинам:
  • Объявлять пустые деструкторы в такой ситуации однозначно нужно.
  • Или же не выпендриваться и включать все нужные заголовки, а потом пить чай пока проект собирается.
  • Пользоваться boost::scoped_ptr вместо std::auto_ptr, так как в boost проверяется, может ли компилятор удалить объект (и получаем ошибку компиляции в случае неудачи).

среда, 12 августа 2009 г.

Кодировки и BOM

Довольно забавно обстоит дело с обработкой компилятором от Microsoft строковых литералов в программах на C++. В случаях с символами, попадающими в первые 7 бит (ANSI) всё ясно - всегда всё нормально. Но с родной кириллицей всё веселее.
И редактор студии, и компилятор умеют определять используемую кодировку. Вот только редактор делает это лучше.
Пример: есть файлы в UTF-8 без BOM, созданные под GNU/Linux, с программой на Qt. g++ сохраняет кодировку в литералах и в памяти программы строка оказывается в кодировке UTF-8. И перевести её потом в QString можно при помощи fromUtf8.
Компилятор же от Microsoft не видя BOM считает, что кодировка файла соответствует локали и тоже оставляет строковые литералы как есть. Но, так он неверно определяет кодировку, вместо unicode-литералов (это те, которые L"текст") в памяти получается мусор.
Пример:

MessageBox(0, L"Привет", L"Мир", MB_OK);



Если же файл сохранить с BOM, то тогда компилятор догадывается что это UTF-8 и переводит обычные строки в локальную кодировку, зато и unicode-строки воспринимает правильно. И прощай один код для двух платформ, придётся пользоваться fromLocal8Bit под Windows и fromUtf8 под GNU/Linux.
Кроме того, g++ хоть и формально умеет работать с UTF-8, но к BOM относится враждебно, отказываясь компилировать, что опять же не позволяет использовать один исходник для обеих платформ.

Вывод: либо смириться и не использовать кириллицу в исходном коде, либо не использоваться Unicode-литералы и пользоваться QString::fromUft8 везде.

Примечание: Стандарт не разрешает использовать национальные символы даже в литералах, вместо этого надо указывать код символа (например '\320'), так что лучше кириллицу в исходниках не использовать вовсе. С другой стороны Qt предлагает удобные утилиты и классы для перевода. Другое дело что это почти всегда неудобно, особенно когда программа пишется только для русскоязычных пользователей и возиться с переводом того что можно сразу сделать на русском не хочется.

четверг, 6 августа 2009 г.

Методички для института

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

Тем не менее, что-то всё же получилось и кому-нибудь может пригодиться.

Первая методичка - про реализацию COM посредством ATL в MSVS. Тут я даже сподобился на интуитивно-понятные видеоматериалы (тут всё очень мелко, но в архиве видео с нормальным разрешением):




Внутри архива - презентация, методичка, проекты к методичке и презентации, а также видео:
Скачать методичку по COM и ATL.7z


Вторая методичка - про автоматизацию MS Office через COM из всё той же MSVS. Тут меня уже на видео не хватило, но проекты и методичку в архиве вы найдёте:

Скачать методичку по MS Office из MSVS.7z

Про переводы строк

Писал сегодня получение информации о системе в windows-части диплома. Была задача получить при помощи Windows API несколько значений и запихнуть их в QString-и, которые используются далее по коду.
Казалось бы, что сложного? Написал вот такой код:

// Определяем имя компьютера
char computerName[200]={0};
DWORD dummy = sizeof(computerName)/sizeof(char);
GetComputerNameA(computerName, &dummy);
computer = QString::fromLocal8Bit(computerName);
// Определяем текущий каталог
char currentDir[400]={0};
GetCurrentDirectoryA(sizeof(currentDir)/sizeof(char), currentDir);
curdir = QString::fromLocal8Bit(currentDir);

Всё хорошо, всё компилируется... и ничего не показывает. В соответствующих строках - пусто.

Перечитываю код - вроде всё верно, должно работать. Значит начинаем отладку.
Отладка, как обычно, начинается с полной пересборки проекта. Rebuild - это просто обязательное действие. Оно гарантирует что мы не будем искать ошибку в неправильном коде, который слинковался в результате ошибки компоновки с приращением
(incremental linking) или неправильно разрешенными зависимостями между файлами.

Конечно же пересборка проблему не устранила. Попытка пройтись по шагам в построчном отладчике тоже ничего не дала - проект собран в release, код перемешан и указатель прыгает как бешенный по каждому F10 (Step over).

Зато просмотр окна disassemble (Alt+8) выявил, что функции наши не вызываются вовсе. Как будто и нет этого кода...
И действительно, получившийся EXE не зависит от функций GetComputerNameA , GetCurrentDirectoryA и прочих.

Чтобы убедиться что в эту ветку кода вообще заходит управление, добавил перед вышеприведённым кодом
искусственный бряк:

__asm int 3;

Эта инструкция предназначена для останова в отладчике.

Проект успешно брякнулся, значит код в этой ветке исполняется. Тут моё подозрение пало на компоновщик, и я некоторое время изучал его опции. Всё было верно.

Круг подозреваемых всё более сужался. Было ясно, что что-то происходит при компиляции. Первая мысль была про неверную оптимизацию,
но я её сразу отбросил.
Подумав некоторое время, я стёр строку комментария "// Определяем имя компьютера".
На моё счастье я забыл описать переменную computer выше и получил ошибку.
Но чудо: ошибка указывала на строку с другим номером!

После этого стало всё ясно:
  1. файл изначально писался под linux-ом и переводы строк были обычные для unix-систем (\n).
  2. эту часть файла я дописывал под windows, вставив из другого файла (\r\n).
  3. компилятор запутался в разных переводах строк и не посчитал за перевод строки символы после комментария. Таким образом, следующие строки считались продолжением однострочного комментария...

Проблема решилась в меню File->Advanced Save Options указанием Line Endings и сохранением файла.

Мораль:
  1. переводы строк тоже имеют значение и могут запутать компилятор.
  2. Умение пользоваться инструментарием вроде Dependency Walker или встроенного дисассемблера MSVS сильно ускоряет локализацию проблемы.

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