*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***

воскресенье, 29 марта 2009 г.

Триггер параллельных потоков для Windows и UNIX

Условные переменные, как и мьютексы, являются базовыми примитивами для синхронизации в параллельном программировании. К сожалению, классическая условная переменная в нотации потоков POSIX (pthread) сложно реализуема в Windows (судя по MSDN Windows таки поддерживают механизм условных переменных на уровне API, но не в XP или 2003, в чем-то более новом, увы). Мне потребовался для одного проекта простейший механизм синхронизации двух потоков: один поток ждет, не занимая ресурсов процессора, и активизируется, только когда другой поток его попросит об этом. Простейший триггер. Конечно, по логике — это обыкновенная условная переменная в упрощенном варианте. Для UNIX это реализуется именно через условную переменную потоков POSIX, а для Windows — через события.

Файл trigger.h:

#ifndef _EXT_TRIGGER_H
#define _EXT_TRIGGER_H

#ifdef WIN32
#include <windows.h>
#else
#include <pthread.h>
#endif

namespace ext {

class Trigger {
public:
Trigger();
~Trigger();

// Функция посылки сигнала потоку,
// ждущему на функции Wait().
void Signal();
// Функция ожидания сигнала.
// Вызов этой функции приводит к блокировке потока до
// получения сигнала от функции Signal().
// Внимание: функция Signal() не должна быть вызвана до
// того, как ждущий поток "сядет" на Wait(). Подобное
// использование ведет к неопределенному поведению.
void Wait();

private:
#ifdef WIN32
HANDLE __handle;
#else
pthread_mutex_t __mutex;
pthread_cond_t __cv;
#endif
// "Защита" от случайного копирования.
Trigger(const Trigger&);
void operator=(const Trigger&);
};

} // namespace ext

#endif
Файл trigger.cpp:
#include "Trigger.h"

namespace ext {

#ifdef WIN32

Trigger::Trigger() {
__handle = CreateEvent(
NULL, // Атрибуты безопасности по умолчанию.
TRUE, // Режим ручной активации события.
FALSE, // Начальное состояния -- неактивное.
NULL // Безымянное событие.
);
}

Trigger::~Trigger() {
CloseHandle(__handle);
}

void Trigger::Signal() {
SetEvent(__handle);
}

void Trigger::Wait() {
// Ждем наступление события.
WaitForSingleObject(__handle, INFINITE);
// "Перезаряжаем" событие.
ResetEvent(__handle);
}

#else // WIN32

Trigger::Trigger() {
pthread_mutex_init(&__mutex, NULL);
pthread_cond_init(&__cv, NULL);
}

Trigger::~Trigger() {
pthread_cond_destroy(&__cv);
pthread_mutex_destroy(&__mutex);
}

void Trigger::Signal() {
pthread_mutex_lock(&__mutex);
pthread_cond_signal(&__cv);
pthread_mutex_unlock(&__mutex);
}

void Trigger::Wait() {
pthread_mutex_lock(&__mutex);
pthread_cond_wait(&__cv, &__mutex);
pthread_mutex_unlock(&__mutex);
}

#endif // WIN32

} // namespace ext
Пространство имен, как обычно, ext, так что меняете по вкусу.
Проверим, как будет работать (естественно, через тест).
Для тестирования также потребуются: класс Thread, класс PreciseTimer и Google Test. О том, как собрать себе компактную версию Google Test в виде всего двух файлов gtest-all.cc и gtest.h уже писал.
Файл trigger_unittest.cpp:
#include <gtest/gtest.h>

#include "trigger.h"
#include "thread.h"
#include "pretimer.h"

// Тестовый поток, который будет "скакать" по указанным ключевым
// точкам, увеличивая значение счетчика.
class TriggerThread: public ext::Thread {
public:
TriggerThread(volatile int& flag, ext::Trigger& trigger) :
__flag(flag), __trigger(trigger)
{}

virtual void Execute() {
// Ждем первого сигнала.
__trigger.Wait();
__flag = 1;
// Ждем второго сигнала.
__trigger.Wait();
__flag = 2;
// Ждем третьего сигнала.
__trigger.Wait();
__flag = 3;
}

private:
volatile int& __flag;
ext::Trigger& __trigger;
};

TEST(Trigger, Generic) {
volatile int flag = 0;
ext::Trigger trigger;

// Создаем поток и запускаем егою
TriggerThread a(flag, trigger);
a.Start();

// Подождем, чтобы поток "сел" на Wait().
ext::PreciseTimer::sleepMs(10);
// Флаг не должен стать 1, так как поток
// должен ждать на Wait().
EXPECT_EQ(0, (int)flag);

// Информируем поток о событии.
trigger.Signal();
// Подождем, чтобы поток успел изменить флаг на 1.
ext::PreciseTimer::sleepMs(10);
// Проверим, как он это сделал.
EXPECT_EQ(1, (int)flag);

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

trigger.Signal();
ext::PreciseTimer::sleepMs(10);
EXPECT_EQ(2, (int)flag);

trigger.Signal();
a.Join();
// Последняя проверка не требует ожидания, так как мы присоединись
// к потоку, и он точно уже завершился.
EXPECT_EQ(3, (int)flag);
}
Компилируем для Windows в Visual Studio:
cl /EHsc /I. /Fetrigger_unittest_vs2008.exe /DWIN32 runner.cpp ^
trigger.cpp trigger_unittest.cpp pretimer.cpp thread.cpp gtest\gtest-all.cc
или в GCC:
g++ -I. -o trigger_unittest_vs2008.exe runner.cpp \
trigger.cpp trigger_unittest.cpp pretimer.cpp thread.cpp gtest/gtest-all.cc
Запускаем:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from Trigger
[ RUN ] Trigger.Generic
[ OK ] Trigger.Generic (31 ms)
[----------] 1 test from Trigger (47 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (78 ms total)
[ PASSED ] 1 test.
Работает.
Внимательный читатель заметит, что по хорошему бы надо протестировать случай, когда функция Signal() вызывается раньше, чем слушающий поток дойдет до Wait(). Как сказано в комментариях, эта ситуация считается логической ошибкой и ведет к неопределенному поведению. В жизни получается так: реализация для Windows считает, что если функция Signal() была вызвана до Wait(), то Wait() просто тут же выходит, как бы получив сигнал сразу при старте. Реализация же под UNIX работает иначе: Wait() отрабатывает только те вызовы Signal(), которые были сделаны после начала самого Wait()'а. Самое настоящее неопределенное поведение. При использовании данного класса надо помнить об этом ограничении.

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

четверг, 19 марта 2009 г.

Google Test Framework 1.3.0

Сегодня вышла новая версия Google Test Framework1.3.0.

Радостно, что авторы воплотили мою идею, когда вся библиотека собирается всего в два файла: gtest-all.cc и gtest.h. Теперь для этого есть специальный скрипт на Питоне. Распаковываем архив gtest-1.30.zip и запускаем:

python scripts\fuse_gtest_files.py . fuse
После этого во вновь созданном подкаталоге fuse будет находиться "упакованная" версия библиотеки в виде двух файлов gtest/gtest-all.cc и gtest/gtest.h. Моя аналогичная, но ручная сборка для предыдущей версии больше неактуальна.

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

#include "gtest/gtest.h"
int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc, argv);
testing::GTEST_FLAG(print_time) = true;
return RUN_ALL_TESTS();
}
Итак, новые возможности версии 1.3.0:
  • поддержка так называемых смертельных тестов для Windows (раньше это работало только под Linux)
  • параметр командной строки --gtest_also_run_disabled_tests для принудительного запуска отключенных тестов
  • возможность распараллеливать запуск тестов на разных машинах
Небольшая программа ниже демонстрируем новые "вкусности" Google Test.

Файл runner.cpp:
#include "gtest/gtest.h"

#include <fstream>
#include <iostream>
#include <cstdlib>

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

// Данная функция, если файл не существует, печатает сообщение
// об ошибке и завершает программу с ненулевым кодом.
void openfile(const char* name) {
std::ifstream is(name);
if (!is) {
std::cerr << "Unable to open the file" << std::endl;
std::exit(1);
}
}

// Тест для функции openfile().
TEST(OpenFileDeathTest, ExitIfNoFile) {
// Задаем заведомо несуществующий файл и смотрим - завершилась
// ли программа с ненулевым кодом. Также проверяем регулярным
// выражением то, что программа напечатала при выходе.
// Мы ожидаем слово "open" среди остального вывода.
ASSERT_DEATH({ openfile("__nofile__"); }, ".*open.*");
}

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

// Данная функция должна падать с assert'ом, если делитель
// равен нулю.
int divide(int a, int b) {
assert(b != 0);
return a / b;
}

// Тест для assert'а в функции divide().
TEST(AssertDeathTest, DivideByZero) {
// Задаем нулевой делитель и смотрим - упала или нет.
// Вывод программы при падении не проверяем.
ASSERT_DEATH({ divide(1, 0); }, "");
}

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

// Данная функция должна при ненулевом коде завершать
// программу, прибавив к заданному коду ошибки 50.
void abandon(int code) {
if (code != 0) std::exit(code + 50);
}

// Тест для функции abandon().
TEST(AbandonDeathTest, ExitCode) {
// Вызываем функцию и смотрим код возврата.
// Вывод программы при выходе не проверяем.
ASSERT_EXIT(abandon(200), testing::ExitedWithCode(250), "");
}

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

// Заведомо неработающий “сломанный” тест.
// Если имя группы тестов или теста в отдельности предварить
// словом DISABLED_, то тест не будет участвовать с запуске.
// Это удобно, когда какой-то тест сломан, времени на его
// отладку нет, но убирать его из тестирования совсем нельзя.
// В это случае его можно отключить. Google Test при каждом
// запуске будет напоминать, сколько имеется отключенных тестов.
// В процессе же работы над тестом можно запускать программу
// с параметром "--gtest_also_run_disabled_tests", который
// будет проверять также и отключенные тесты.
TEST(BadTest, DISABLED_Test) {
FAIL();
}

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

int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc, argv);
// Принудительно печатаем время работы тестов.
testing::GTEST_FLAG(print_time) = true;
return RUN_ALL_TESTS();
}
Компилируем в Visual Studio:
cl /EHsc /I. /Ferunner_vs2008.exe /DWIN32 runner.cpp gtest\gtest-all.cc
Запускаем:
[==========] Running 3 tests from 3 test cases.
[----------] Global test environment set-up.
[----------] 1 test from OpenFileDeathTest
[ RUN ] OpenFileDeathTest.ExitIfNoFile
[ OK ] OpenFileDeathTest.ExitIfNoFile (31 ms)
[----------] 1 test from OpenFileDeathTest (31 ms total)

[----------] 1 test from AssertDeathTest
[ RUN ] AssertDeathTest.DivideByZero
[ OK ] AssertDeathTest.DivideByZero (31 ms)
[----------] 1 test from AssertDeathTest (31 ms total)

[----------] 1 test from AbandonDeathTest
[ RUN ] AbandonDeathTest.ExitCode
[ OK ] AbandonDeathTest.ExitCode (32 ms)
[----------] 1 test from AbandonDeathTest (32 ms total)

[----------] Global test environment tear-down
[==========] 3 tests from 3 test cases ran. (94 ms total)
[ PASSED ] 3 tests.

YOU HAVE 1 DISABLED TEST
Отлично, все работает. Также не забудем, что у нас таки есть один отключенный тест. Его можно запустить принудительно, использовав ключ --gtest_also_run_disabled_tests:
runner_vs2008.exe --gtest_also_run_disabled_tests
Получим следующее:
[==========] Running 4 tests from 4 test cases.
[----------] Global test environment set-up.
[----------] 1 test from OpenFileDeathTest
[ RUN ] OpenFileDeathTest.ExitIfNoFile
[ OK ] OpenFileDeathTest.ExitIfNoFile (31 ms)
[----------] 1 test from OpenFileDeathTest (31 ms total)

[----------] 1 test from AssertDeathTest
[ RUN ] AssertDeathTest.DivideByZero
[ OK ] AssertDeathTest.DivideByZero (32 ms)
[----------] 1 test from AssertDeathTest (32 ms total)

[----------] 1 test from AbandonDeathTest
[ RUN ] AbandonDeathTest.ExitCode
[ OK ] AbandonDeathTest.ExitCode (31 ms)
[----------] 1 test from AbandonDeathTest (31 ms total)

[----------] 1 test from BadTest
[ RUN ] BadTest.DISABLED_Test
runner.cpp(72): error: Failed
[ FAILED ] BadTest.DISABLED_Test (0 ms)
[----------] 1 test from BadTest (0 ms total)

[----------] Global test environment tear-down
[==========] 4 tests from 4 test cases ran. (94 ms total)
[ PASSED ] 3 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] BadTest.DISABLED_Test

1 FAILED TEST
Под занавес отмечу, что появился еще один новый ключ командной строки --help для печати на экран всех весьма многочисленных параметров Google Test.

Я уже обновился до версии 1.3.0, а вы?

Включение/выключение proxy в Internet Explorer

Иногда таки приходится пользоваться Internet Explorer'ом (всякие сайты кривые, web-интерфейсы некоторых роутеров и т.д.).

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

Кстати, гугловский Chrome по каким-то причинам использует настройки интернета от IE (системные для всего Windows), поэтому все сказанное актуально и для него.

Итак, привожу два скрипта для включения и отключения proxy в системных настройках интернета в Windows.

Файл iepon.cmd:

reg add "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyEnable /t REG_DWORD /d 1 /f

Файл iepoff.cmd:

reg add "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyEnable /t REG_DWORD /d 0 /f

Я проверял это только в Windows XP.

вторник, 17 марта 2009 г.

Programming WTF

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

Потом можно для снятия умственного напряжения и для еще большого подняния самооценки полистать сообщество "Programming WTF".

Начав с известной нетленки для проверки условия i < 10:
uint i;
...
if (i.ToString().Length == 1)
{
...
}
можно постепенно усиливать ощущения...
std::string str1;
std::string str2;
...
if (!strcmp(str1.c_str(), str2.c_str()))
{
...
}
вставляя в код противопехотные мины...



различного радиуса поражения...
#define bool BOOL
и убойной силы.
<?
define( "FALSE", -1 );
define( "TRUE", 0 );
?>
А вот это для настоящих гурманов и знатоков своего дела:
#define sizeof(x) rand()
После того, как вы, обойдя вашу систему ревизий кода, чтобы никто не заметил засады, добавили это в какой-нибудь тихий, но повсеместно используемый файл ваших коллег смело идите покурить. Не думаю, что удасться выкурить в тишине хотя бы одну сигарету.

Теперь ваши коллеги тоже снимут стресс и напряжение.

воскресенье, 15 марта 2009 г.

Какой конструктор когда вызывается в С++

С++ имеет весьма разнообразный синтаксис для конструирования объектов. Надо признать, что порой этот синтаксис весьма неочевиден, и многие вещи надо просто знать, нежели догадаться, как они работают. Например:
class T {...};
...
T t = T(1);
По очевидной логике вещей данный код должен при создании экземпляра класса T вызвать конструктор по умолчанию (без аргументов), затем создать временный объект с помощью конструктора с одним аргументом и скопировать его в исходный объект перегруженным оператором копирования (или может конструктором копирования? ведь слева и справа объекты явно типа T...).

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

Именно для таких случаев я обычно даю следующий пример, который покрывает часто используемые варианты создания объектов. Разобрав его один раз целиком, можно использовать его как подсказку в будущем, когда опять возникает вопрос "а что ж здесь будет вызвано: конструктор или оператор копирования?...".

Итак, файл ctor.cpp:

#include <iostream>

class T {
public:
T() { std::cout << "T()" << std::endl; }
T(int) { std::cout << "T(int)" << std::endl; }
T(int, int) { std::cout << "T(int, int)" << std::endl; }
T(const T&) { std::cout << "T(const T&)" << std::endl; }
void operator=(const T&)
{ std::cout << "operator=(const T&)" << std::endl; }
};

int main() {
std::cout << "T t1 : "; T t1;
std::cout << "T t2(1) : "; T t2(1);
std::cout << "T t3 = 1 : "; T t3 = 1;
std::cout << "T t4 = T(1) : "; T t4 = T(1);
std::cout << "T t5(1, 2) : "; T t5(1, 2);
std::cout << "T t6 = T(1, 2) : "; T t6 = T(1, 2);
std::cout << "T t7; t7 = 1 : "; T t7; t7 = 1;
std::cout << "T t8; t8 = T(1): "; T t8; t8 = T(1);
std::cout << "T t9(t8) : "; T t9(t8);
std::cout << "T t10 = 'a' : "; T t10 = 'a';
return 0;
}
Компилируем, например в Visual Studio:
cl /EHsc ctor.cpp
и запускаем:
T t1           : T()
T t2(1) : T(int)
T t3 = 1 : T(int)
T t4 = T(1) : T(int)
T t5(1, 2) : T(int, int)
T t6 = T(1, 2) : T(int, int)
T t7; t7 = 1 : T()
T(int)
operator=(const T&)
T t8; t8 = T(1): T()
T(int)
operator=(const T&)
T t9(t8) : T(const T&)
T t10 = 'a' : T(int)
Видно, что во всех этих "разнообразных" способах создания объекта всегда вызывался непосредственно конструктор, а не оператор копирования. Оператор же копирования был вызван только когда знак присваивания использовался явно в отдельном от вызова конструктора операторе. То есть знак "=", используемый в операторе конструирования объекта так или иначе приводит к вызову конструкторов, а не оператора копирования. И это происходит вне зависимости от какой-либо оптимизации, проводимой компилятором.

Также интересно, как был создана переменная t10. Видно, что для символьной константы компилятор "подобрал" наиболее подходящий конструктор. Неявным образом был вызвал конструктор от int. Если подобное поведение не входит в ваши планы, и вам совсем не нужно, чтобы конструктор от int вызывался, когда идет попытка создать объект от типа, который может быть неявно преобразован в int, например char, то можно воспользоваться ключевым словом explicit:

class T {
public:
...
explicit T(int) { std::cout << "T(int)" << std::endl; }
...
};
Это запретит какое-либо неявное преобразования для аргумента этого конструктора.

Вообще практика объявления любого конструктора с одним параметром со модификатором explicit является весьма полезной, и позволяет избежать некоторых неприятных сюрпризов, например, если вы хотели вызвать конструктор строки от типа char, предполагая создать строку, состоящую только из одного символа, а получилось, что этот класс не имеет такого конструктора. Зато есть конструктор от int, делающий совершенно не то, что вам нужно. Вот и будет сюрприз в виде символьной константы, истолкованной как целое число.

Я обычно по умолчанию пишу explicit для конструкторов с одним параметром, и очень редко приходится потом убирать этого слово. Тут как со словом const — сначала можно написать, а потом уже думать нужно ли тут его убрать или нет.

среда, 11 марта 2009 г.

"Легкая" интеграция Perforce в Visual Studio

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

Например, в Perforce, как и во многих других системах контроля версий это решается выставлением атрибута read-only по умолчанию на все локальные файлы, находящиеся под контролем версий. Это позволяет исключить “случайное” изменение. Если надо изменить файл, то он помечается как рабочий (в Perforce "p4 edit имя_файла"). После этого с файла снимается флаг read-only, и Perforce добавляет его список "открытых" файлов. Теперь, когда вы хотите узнать, какие файлы у вас сейчас на редактировании, то команда Perforce "p4 opened" моментально выдаст список без глобального сканирования изменений. Также "p4 diff" столь же мгновенно отобразит сами изменения. По началу, такой подход может напрягать, и я часто слышу жалобы типа проще открыть сразу весь проект по маске через p4 edit ..., поработать спокойно, а уже под занавес сделать полное сканирование изменений для определений реально измененных файлов и только их отправить на сервер командой "p4 submit".

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

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

Конечно, работая в студии, не всегда удобно постоянно лазить в командную строку для "p4 edit ...". Хочется делать это прямо из меню. Способ есть, и никакой шибко умный плагин не нужен.

Идем в меню Tools, затем в "External tool...". Далее создаем кнопкой "Add" элементы "&P4 Edit" (Arguments: "edit $(ItemPath)"), "&P4 Revert" (Arguments: "revert $(ItemPath)"), "&P4 Diff" (Arguments: "diff $(ItemPath)") по аналогии с картинкой.

Теперь для открытия за запись файла из текущего окна редактирования надо выбрать в меню Tools->P4 Edit, для отката изменений — Tools->P4 Revert, а для просмотра изменений — Tools->P4 Diff.

Вывод этих команд сохраняется в окне Output.

По вкусу можно добавить аналогичным образом любую команду из арсенала командного клиента Perforce p4, но именно эти обычно нужны в 99% случаев. Я обычно назначаю горячие клавиши на эти пункты меню. Для остального можно уже слазить в командную строку или в графический клиент Perforce (P4V или P4Win).

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

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

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

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

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

Приятно, что графический клиент Perforce P4V существует не только под Windows (в отличие от старого P4Win). Он есть под Linux, Solaris, FreeBSD и Mac. Если надо работать сразу под несколькими системам, можно запустить у себя на машине X-сервер, и видеть клиентов со всех платформ одновременно. P4V использует Qt, посему выглядит почти одинаково на всех системах.

Кстати, выскажу свое субъективное мнение на тему контроля версий. Если бы мне продавали исходники чего-то сложного, то для меня было бы очень важным критерием среди прочих наличие не только вылизанной для продажи "последней супер версии", но и всей истории разработки, в формате какой-нибудь системы контроля версий и баг-трекинга.

понедельник, 9 марта 2009 г.

sizeof('a') в С и C++

В очередной раз узнал для себя новый потенциальный вопрос для "прогиба" на каком-нибудь тесте по языку С.

Программа x.c:
#include <stdio.h>
int main() {
char s[4];
s[0] = 'C'; s[1] = s[2] = '+'; s[3] = 0;
s[sizeof(' ') ^ 5] = 0;
printf("%s\n", s);
return 0;
}
Компилируем и запускаем.

Visual Studio:
cl x.c && x
или в Cygwin:
gcc -o x x.c && x
Имеем следующий результат:
C
А теперь так:
cl /TP x.c && x
или в Cygwin:
g++ -o x x.c && x
Теперь программа печает иное:
C++
Результат повеселил некоторых моих коллег.

Нашел небольшой список еще некоторых "отличий" С и С++, но, пожалуй, этот самый неявный, а значит потенциально опасный.

воскресенье, 8 марта 2009 г.

Загрузка Linux без ядра за 25 секунд

Естественно, загрузиться Linux совсем без ядра не может. Но он может загрузиться не имея в начале процесса загрузки ядра в двоичном виде. А откуда же берется ядро? Ядро компилируется прямо при загрузке!

Вы думаете, такая загрузка будет длиться годы? ну или хотя бы минуты? Нет. На все про все — 25 секунд с хвостиком.

Итак, по порядку.

Все знают QEMU — бесплатная виртуальная машина. Из нее, например, вырос пакет VirtualBox, а KVM унаследовал интерфейс командной строки. Это чистый виртуализатор без всяких там “пара-” приставок. Из-за этого работает небыстро и для критичных по скорости задач слабо применимо, но с другой стороны из-за “чистоты” виртуализации работает на многих платформах и виртуализирует многие платформы, а не только Intel, как большинство “быстрых” виртуальных машин. Из-за все сказанного, QEMU идеален для всякого рода экспериментов и нестандартных задач.

Но мы отвлеклись. Автор QEMU — Fabrice Bellard — написал еще нескольно занимательных программ.

Одной из них является TCC — Tiny C Compiler. Это ультра быстрый и ультра маленький компилятор С. Сразу возникает подозрение — слово “tiny” в название, да еще и “ультра быстрый” и “ультра маленький”. Главный вопрос — какие у него ограничения?

Как заявляет автор, TCC полностью поддерживает стандарт языка С вплоть до ISO C99 включительно, но целевая платформа только x86. Компилятор имеет также мини версию системной библиотеки libc. Когда это возможно, компилятор совмещает фазы компилирования, ассемблирования и линковки для дополнительного ускорения, хотя поддерживаются стандартные ABI и можно подлинковать что-то готовое.

Компилятор доступен в исходных текстах и в двоичном виде под Windows. Скомпилировать его можно вручную, например, самим же TCC.

Нужно на чем-нибудь проверить TCC, на чем-нибудь нетривиальном. Ядро Linux'а является весьма сложным и большим проектом, это его сборка была бы отличной проверкой.

TCC не только успешно собирает ядро, но и делает это до 9 раз быстрее, чем GCC (естественно, речь идет только о платформе x86).

Невероятная скорость компиляции позволяет использовать TCC как компилирующий “интерпретатор” скриптов. Если добавить первой строкой вашей программы на С строчку “#!/usr/local/bin/tcc –run” и установить флаг “executable” на исходник, то ваша программа будет запущена в UNIX’е прямо из исходного текста, будучи скомпилированной на лету.

Мы подходим к сути. Автор предлагает вариант загрузки Linux, когда ядро компилируется прямо в процессе загрузки из исходных текстов. Проект называется TCCBOOT. Можно скачать ISO имидж (около 6 мегабайт), записать на болванку, загрузиться с нее и увидеть все самому. Что я и сделал.

Загрузчик ISOLINUX запускает мини образ, в котором содержится TCC, исходники ядра и минимальное окружение для запуска командного интерпретатора под скомпилированным на лету ядром.

Поехали…

Старт, запустился ISOLINUX, началась компиляция ядра:




Все, за 25.4 секунды ядро скомпилировано, запущено, и загружена минимальная UNIX система:



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

Эксперимент проводился на ноутбуке Core 2 1GHz, 2GB RAM.

Я был очень впечатлен. А если в TCC нормально поддержать многопроцессорность? Тут недалеко и до полностью функциональной операционной системы, у которой нет двоичного представления до загрузки.

пятница, 6 марта 2009 г.

Саморазархивирующиеся архивы для UNIX

Когда ты поставляешь софт под UNIX, редко бывает ситуация, когда на стороне заказчика в группе сопровождения вообще нет людей, знакомых с UNIXом. Так или иначе, стиль этой операционной системы заставляет работать с командной строкой и знать хотя бы базовые команды. Тут порой недостаточно просто "кликнуть на файл с дистрибутивом". Не буду говорить, как это хорошо или плохо для заказчика — это его выбор, но для нас, поставщиков, это удобно. Удобно, когда "с той стороны" есть люди, которым достаточно сказать типа возьмите tar-архив, разверните в такой то директории, проверьте права и замените такие-то бинарники.

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

Небольшой анализ привел меня к makeself. В двух словах — это саморазархивирующиеся shell-скрипты. То есть вы готовите процедуру установки, сводите все к наличию каталога со всеми необходимыми файлами, и, если нужно, скриптом, которые надо запустить после разархивации. Все как у так называемых SFX (self extract) модулей для WinRAR, например. Прелесть в том, что в итоге вы получаете одиночный файл, который является абсолютно стандартным shell-скриптом, работающим в большом количестве типов UNIX, и который также содержит внутри себя архив с вашими файлами. Все, что нужно сделать на стороне клиента, это запустить этот файл.

Допустим, вы подготовили ваш дистрибутив в каталоге /home/sandbox/intallation. Также у вас есть скрипт ./setup, который необходимо запустить после разархивации для локальной настройки, например. Все что вы делаете:
makeself.sh /home/sandbox/installation megasoft-0.0.1.sh "Mega Software 0.0.1" ./setup
Данная команда создаст файл magesoft-0.0.1.sh, в который упакуется содержимое каталога /home/sandbox/intallation и скрипт ./setup. Теперь все, что надо сделать на стороне клиента, это запустить это файл командой:
. ./megasoft-0.0.1.sh
Скрипт разархивирует собственное содержимое и запустит ваш скрипт setup, который сможет окончательно настроить установку.

makeself позволяет использовать для компрессии стандартные средства UNIX на выбор — compress, gzip, bzip2. Также содержимое архива дополнительно защищается контрольными суммами: MD5 или CRC. Это может быть полезно, если вы не используете компрессию, а целостность данных проверять все же хотите.

Список же поддерживаемых типов UNIX для текущей версии 2.1 весьма внушителен:
  * Linux (all distributions)
* Sun Solaris (8 tested)
* HP-UX (tested on 11.0 and 11i on HPPA RISC)
* SCO OpenUnix and OpenServer
* IBM AIX 5.1L
* MacOS X (Darwin)
* SGI IRIX 6.5
* FreeBSD
* UnicOS / Cray


Напомню ссылку на makeself еще раз — http://megastep.org/makeself/

четверг, 5 марта 2009 г.

Как обойтить без макроса NOMINMAX

В комментариях к посту про проблему конфликта имен STL'евских std::min и std::max с одноименными макросами из файла windows.h мне подсказали интересное решение.

Если вместо, например, std::max(a, b) написать (std::max)(a, b), то результат работы препроцессора выглядит так:
#line 3 "minmax.cpp"
int main() {
int a = (std::min)(10, 20);
return 0;
}
вместо:
#line 3 "minmax.cpp"
int main() {
int a = std::(((10) < (20)) ? (10) : (20));
return 0;
}
и конфликта не происходит. Все компилируется без проблем.

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


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

QueryPerformanceCounter() на мультиядерных и виртуальных системах

Как я обещал, рассказываю про мои приключения с классом PreciseTimer.

Мой класс PreciseTimer предназначен для работы с миллисекундными интервалами времени. Реализация под Windows основана на использовании функций QueryPerformanceFrequency() и QueryPerformanceCounter().

Этот класс активно используется в некоторых наших проектах. Также, в силу некоторых обстоятельств, мы активно используем виртуальные машины для тестовых сборок. И, например, сборка под Windows 64-бита производится под VirtualBox. И вот очередной релиз-кандидат ушел в тестирование. Немедленно мне посыпались жалобы, что сборка не работает под 64-битным Windows под виртуальной машиной.

Я запретил тестерам временно отключать тест и начал проверять все сам. На реальных машинах все работает. Начал гонять на виртуальных. На VMWare тоже глючит. Тест PreciseTimer.MeasurementAccuracy выдает ошибку типа:
c:\sandbox\test\PreTimer_unittest.cpp(22): error: Value of: delta <= allowed_delta_ms
Actual: false
Expected: true
Delta (100) > than 10
[ FAILED ] PreciseTimer.MeasurementAccuracy (110 ms)
Получается, что задержка в 100 миллисекунд была измерена практически как нулевая.

Я заподозрил функцию QueryPerformanceCounter(). Написал еще один кондовый тест:
TEST(PreciseTimer, MillisecCounter) {
monitor::PreciseTimer timer;
monitor::PreciseTimer::Counter a = timer.millisec();
timer.sleepMs(10000);
monitor::PreciseTimer::Counter b = timer.millisec();
EXPECT_EQ(10000, b - a);
}
Этот тест делает видную глазом задержку в 10 секунд (чтобы исключить проблему в самой задержке) и затем проверят показания таймера.

Итак, на реальной машине тест выдает следующее:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from PreciseTimer
[ RUN ] PreciseTimer.MillisecCounter
c:\sandbox\test\PreTimer_unittest.cpp(17): error: Value of: b - a

Actual: 9995
Expected: 10000
[ FAILED ] PreciseTimer.MillisecCounter
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran.
[ PASSED ] 0 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] PreciseTimer.MillisecCounter

1 FAILED TEST
Тест, конечно, сбоит, но тут четко видно, что требуемая задержка в 10000 миллисекунд (10 секунд) измерена как 9995 миллисекунд. Понятно, тут невозможно измерить точь в точь, но суть работает верно.

А вот, что я получил на виртуальное машине:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from PreciseTimer
[ RUN ] PreciseTimer.MillisecCounter
c:\sandbox\test\PreTimer_unittest.cpp(17): error: Value of: b - a
Actual: 90
Expected: 10000
[ FAILED ] PreciseTimer.MillisecCounter
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran.
[ PASSED ] 0 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] PreciseTimer.MillisecCounter

1 FAILED TEST
Задержка в 10000 миллисекунд была измерена всего как 90. Вот и причина сбоя — функция QueryPerformanceCounter(). Полчаса работы.

Затем я поискал в интернете на тему проблем у функции QueryPerformanceCounter() на виртуальных машинах и нашел объяснение в MSDN. Корень проблемы, как оказалось, был не конкретно в виртуальных машинах, а в "старом" биосе и в использовании мультиядерных систем. На наших реальных мультиядерных машинах все работало, так как, видимо их биос был "нормальным".

В итоге проблема решилась добавлением параметра /usepmtimer в файл c:\boot.ini, как рекомендуется в найденной статье. После этого все тесты заработали как положено.

Я включил подробное описание проблемы в Release Notes, чтобы клиенты не наступили на эти грабли, и инцидент был исчерпан. Еще полчаса работы. Итого час на все.

А теперь вдумайтесь в произошедшее. Проблема была локализована и исправлена не то, чтобы до релиза. Она была локализована даже до тестового запуска. Лично я вот ну ни как не ожидал, что в Windows функция QueryPerformanceCounter() почему-то как-то особенно работает на мультиядерных системах со "старым" биосом (видимо биосы VMWare и VirtualBox как раз подходят под эту категорию). А вот как бы искал эту проблему потом? уже на реальной работающей системе. Одно из применений этого класса у нас, это измерения временных данных по транзакциям. Да я потратил был потом полжизни для поиска этой "маленькой проблемки", случись она у реального клиента.

Пишите тесты! Это экономит не только деньги, но самое драгоценное — ваши нервы.


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

вторник, 3 марта 2009 г.

Фредерик Брукс, "Мифический человеко-месяц или как создаются программные системы"

Фредерик Брукс

Мифический человеко-месяц или как создаются программные системы.



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

В итоге, я не отрываясь внимательно прочитал ее до конца часа за два, и некоторые куски потом пересматривал. По моему мнению, это надо прочесть любому программисту или руководителю программистов. Несмотря на то, что книге скоро стукнет 35 лет, и ее переиздание 1995 года практически повторяет оригинальное, в новом издании всего добавили пару глав, но старые главы остались в исходном виде. Даже когда автор употребляет несколько угловатые в наши дни выражения типа "выйти на машину" или "обратиться к журналу с дисплейного терминала" — это совершенно не искажает сути. Видимо из-за собственной самоуверенности, я считал, что такие значимые для меня вещи как многоуровневое тестирование, самодокументируемый код, контроль версий, готовность вносить изменения и т.д. придуманы совершенно недавно, можно сказать, на моих глазах. "Ах какой удар от классика!" — все это придумано и применялось уже тогда.

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

Вывод

Два часа прочтения будут отзываться в вас еще долго, а необычные сегодня картины вычислительной техники "тех" дней только помогут кристаллизовать абсолютно актуальные до сих пор выводы Фредерика Брукса.


Посты по теме:

воскресенье, 1 марта 2009 г.

Unit-тестирование в языке С

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

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

Мир языка С++ не такой дружественный к тестированию, как например, мир Java, C# или мир интерпретаторов. Главная причина — крайне слабый механизм интроспекции, то есть возможности исследования двоичного кода в плане получения информации о структуре исходных текстов. В Java, например, есть "The Reflection API", с помощью которого можно прямо на основе скомпилированных классов создать тестовую среду (понять иерархию классов, типа аргументов и т.д.). В С++ приходится многое закладывать в исходный текст на этапе его создания, чтобы облегчить будущее тестирование.

А что же мы имеем в С? Тут, как мне кажется, разрыв в удобстве тестирования по отношению к С++ в разы больше, чем между С++ и Java, например. Причин море: процедурная модель вместо объектно-ориентированной, отсутствие интроспекции вообще, крайне слабая защита при работе с памятью и т.д.

Но шансы все же остались. Я начал поиск готовых библиотек для unit-тестирования в С. Например, есть библиотека MinUnit, длиной в четыре строки. Вполне жизненно. Следующий вполне себе вариант — это CUnit. Тут даже есть продвинутый консольный интерфейс.

Перебрав еще несколько вариантов, я остановился на гугловской библиотеке cmockery. Мне понравилось, что библиотека, несмотря на весьма сложный код, успешно компилируются не только в Visual Studio и GNU C, но и “родными” компиляторами AIX, HP-UX, SunOS и некоторых других экзотических зверей. Также библиотека умеет отлавливать утечки памяти, неправильную работу с распределенными кусками памяти (так называемые buffer over- и under- run). Еще в cmockery есть зачатки mock-механизмов, то есть когда задаются предполагаемые сценарии выполнения тестируемого блока, и потом результаты тестового прогона сверяются с предполагаемым сценарием. Mock-возможности я не буду пока рассматривать в данной статье. Про это стоит написать отдельно.

На текущий момент актуальной версией cmockery является 0.1.2. Из всего архива реально нужны только два файла: cmockery.c и cmockery.h. Можно, конечно, собрать библиотеку как положено, в двоичном виде, но я предпочитаю работать всегда с исходными текстами, благо компилируется очень быстро (это ж не С++).

Желающие, могут скачать мою сборку cmockery. В этом архиве только необходимые два файла cmockery.c и cmockery.h. Также в файл cmockery.h я внес небольшое изменение, связанное к тем, что функция IsDebuggerPresent() почему-то явно объявлена в заголовочных файлах только в Visual Studio 2008. Для студии 2003 и 2005 надо вручную объявлять прототип, иначе при линковке вылезает сообщение:

error LNK2019: unresolved external symbol _IsDebuggerPresent referenced in function __run_test

Я отрапортовал об этом досадном недочете авторам, и пока новый релиз cmockery не вышел, можно пользоваться моей сборкой, которая без предупреждений компилируются в любой студии.

Теперь пример реального использования cmockery.

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

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

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

Итак, библиотека cstring. Тут можно создавать в некоторые "объекты", реализованные через структуры, которые представляют собой "строки". Такая "строка" может создаваться либо в стеке (автоматическая переменная), либо в куче. Также предоставляется набор разнообразных базовых функций: определение длины, копирование, склейка, интерфейс со строками языка С (char *) и т.д. Как я уже сказал, для демонстрации системы тестирования я оставил только несколько функций.

Заголовочный файл cstring.h:

#ifndef _CSTRING_H
#define _CSTRING_H

#define _decl_string_t(N) \
struct { \
int sz; \
char data[N]; \
}

typedef _decl_string_t(1) string_t;

/**
* Объявление строки в форме автоматической переменной в стеке.
* Длина строки инициализируется нулем.
*/
#define decl_string_t(name, size) _decl_string_t(size) name = { 0 }

/**
* Создание новой строки в куче.
*/
string_t* string_new(int sz);

/* Трюк с дублированием имен функций, начинающихся с символа '_'
* требуется для подавление предупреждений компилятора о преобразовании
* типов.
*/

/**
* Удаление строки из кучи.
*/
#define string_delete(str) _string_delete((string_t*)str)
void _string_delete(string_t* str);

/**
* Текущая длина строки.
*/
#define string_length(str) _string_length((const string_t*)str)
int _string_length(const string_t* str);

/**
* Изменение длины строки.
*/
#define string_resize(str, sz) _string_resize((string_t*)str, sz)
int _string_resize(string_t* str, int sz);

/**
* Копирование строки из строки С, завершающейся нулем.
*/
#define string_from_c_str(dst, src) _string_from_c_str((string_t*)dst, src)
string_t* _string_from_c_str(string_t* dst, const char* src);

/**
* Добавление символа в строку.
*/
#define string_append_ch(str, ch) _string_append_ch((string_t*)str, ch)
string_t* _string_append_ch(string_t* str, char ch);

/**
* Превращение строки в строку С без добавления нуля на конце.
*/
#define string_data(str) str->data

/**
* Превращение строки в строку С с нулем на конце.
*/
#define string_c_str(str) _string_c_str((string_t*)str)
char* _string_c_str(string_t* str);

#endif
Файл cstring.c:
#include <stdlib.h>

#include "cstring.h"

/**
* Подготовительная площадка для тестирования.
* Если задан макрос UNIT_TESTING, то функции работы с кучей подменяются
* на тестовые.
*/
#if UNIT_TESTING
extern void* _test_malloc(const size_t size, const char* file, const int line);
extern void* _test_calloc(const size_t number_of_elements, const size_t size,
const char* file, const int line);
extern void _test_free(void* const ptr, const char* file, const int line);

#define malloc(size) _test_malloc(size, __FILE__, __LINE__)
#define calloc(num, size) _test_calloc(num, size, __FILE__, __LINE__)
#define free(ptr) _test_free(ptr, __FILE__, __LINE__)
#endif // UNIT_TESTING

/**
* Создание новой строки в куче. Трюк "sizeof(string_t)" используется, чтобы
* правильно отработать ситуацию, если из-за выравнивания между элементами
* структуры string_t 'sz' и 'data' вдруг появится промежуток.
*/
string_t* string_new(int sz) {
return malloc(sizeof(string_t) + sz - 1);
}

/**
* Удаление строки из кучи.
*/
void _string_delete(string_t* str) {
free((void *)str);
}

/**
* Текущая длина строки.
*/
int _string_length(const string_t* str) {
return str->sz;
}

/**
* Изменение длины строки.
*/
int _string_resize(string_t* str, int sz) {
return str->sz = sz;
}

/**
* Копирование строки из строки С, завершающейся нулем.
*/
string_t* _string_from_c_str(string_t* dst, const char* src) {
int sz = strlen(src);
memcpy(dst->data, src, sz);
dst->sz = sz;
return dst;
}

/**
* Добавление символа в строку.
*/
string_t* _string_append_ch(string_t* str, char ch) {
str->data[str->sz++] = ch;
return str;
}

/**
* Превращение строки в строку С с нулем на конце. Фактически,
* в тело строки добавляется ноль и возвращается указатель на данные.
*/
char* _string_c_str(string_t* str) {
str->data[str->sz] = 0;
return string_data(str);
}
Как вы заметили, в коде есть специальный блок, ограниченный макросом UNIT_TESTING. Ничего не поделаешь, в языке С приходится "готовить" код к потенциальному тестированию и вставлять фрагменты, позволяющие тестовой среде работать с этим кодом. Этот блок, если задан макрос UNIT_TESTING, переопределяет функции работы с кучей, чтобы можно было перехватывать их вызовы. Подменяющие функции _test_malloc(), _test_calloc() и _test_free() предоставляются библиотекой cmockery.

Теперь файл тестов cstring_unittest.c:

#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmockery.h>

#include "cstring.h"

/**
* Тестируем декларацию строки длиной 20 в виде автоматической
* переменной, добавляем в нее два символа, обрезаем строку
* до длины в один байт и проверяем, добавился ли 0 при преобразовании
* в строку С.
*/
void string_c_str_test(void **state) {
decl_string_t(a, 20);
a.data[0] = 'a';
a.data[1] = 'b';
a.sz = 1;
assert_memory_equal("a\0", string_c_str(&a), 2);
}

/**
* Тестируем изменение длины строки.
*/
void string_resize_test(void **state) {
decl_string_t(a, 20);
a.sz = 2;
string_resize(&a, 1);
assert_int_equal(1, string_length(&a));
}

/**
* Тестируем добавление символа путем сравнения со строками С
*/
void string_append_ch_test(void **state) {
decl_string_t(a, 20);
assert_string_equal("", string_c_str(&a));
assert_string_equal("a", string_c_str(string_append_ch(&a, 'a')));
assert_string_equal("ab", string_c_str(string_append_ch(&a, 'b')));
}

/**
* Тестируем декларацию строки в виде автоматической переменной.
* Длина строки сразу после декларации должна быть нулевой.
*/
void string_declare_test(void **state) {
decl_string_t(a, 20);
assert_int_equal(0, string_length(&a));
}

/**
* Тестируем размещение новой строки в куче и ее удаление из нее.
*/
void string_heap_allocation_test(void **state) {
string_t* a = string_new(20);
string_delete(a);
}

/**
* Тестируем копирование строки из строки С с нулем на конце.
*/
void string_from_c_str_test(void **state) {
string_t* a = string_new(8);
string_from_c_str(a, "12345678");
assert_int_equal(8, string_length(a));
string_delete(a);
}

/**
* Создаем список тестов и запускаем их.
*/
int main(int argc, char* argv[]) {
const UnitTest tests[] = {
unit_test(string_declare_test),
unit_test(string_c_str_test),
unit_test(string_append_ch_test),
unit_test(string_heap_allocation_test),
unit_test(string_from_c_str_test),
unit_test(string_resize_test),
};
return run_tests(tests);
}
Схема очень похожа на любое другое xUnit тестирование: каждый тест проверяет какой-то один функциональный элемент, тесты объединяются в группы и запускаются автоматически все вместе. Правда, из-за ограничений языка С каждый тест приходится вручную добавлять в список запуска, увы.

Как я уже сказал, для компиляции потребуются файлы cmockery.c и cmockery.h (см. выше). Эти файлы можно положить в текущий каталог.

Компилируем в Visual Studio:

cl /DUNIT_TESTING /I. cstring_unittest.c cstring.c cmockery.c
Если все скомпилировалось нормально, то запускаем файл cstring_unittest:
string_declare_test: Starting test
string_declare_test: Test completed successfully.
string_c_str_test: Starting test
string_c_str_test: Test completed successfully.
string_append_ch_test: Starting test
string_append_ch_test: Test completed successfully.
string_heap_allocation_test: Starting test
string_heap_allocation_test: Test completed successfully.
string_from_c_str_test: Starting test
string_from_c_str_test: Test completed successfully.
string_resize_test: Starting test
string_resize_test: Test completed successfully.
All 6 tests passed
Все тесты отработали правильно.

Но неинтересно, когда все работает. Внесем в тест библиотеки "случайные ошибки". Каждую из них можно спокойно допустить непреднамеренно. Строки с ошибками я пометил комментариями со словом "ОШИБКА (!)". Посмотрим, как cmockery справится с этим.

Файл cstring.c с "ошибками":

#include <stdlib.h>

#include "cstring.h"

/**
* Подготовительная площадка для тестирования.
* Если задан макрос UNIT_TESTING, то функции работы с кучей подменяются
* на тестовые.
*/
#if UNIT_TESTING
extern void* _test_malloc(const size_t size, const char* file, const int line);
extern void* _test_calloc(const size_t number_of_elements, const size_t size,
const char* file, const int line);
extern void _test_free(void* const ptr, const char* file, const int line);

#define malloc(size) _test_malloc(size, __FILE__, __LINE__)
#define calloc(num, size) _test_calloc(num, size, __FILE__, __LINE__)
#define free(ptr) _test_free(ptr, __FILE__, __LINE__)
#endif // UNIT_TESTING

/**
* Создание новой строки в куче. Трюк "sizeof(string_t)" используется, чтобы
* правильно отработать ситуацию, если из-за выравнивания между элементами
* структуры string_t 'sz' и 'data' вдруг появится промежуток.
*/
string_t* string_new(int sz) {
return malloc(sizeof(string_t) + 1 - 1); // (ОШИБКА!) "Неверная" длина.
}

/**
* Удаление строки из кучи.
*/
void _string_delete(string_t* str) {
// (ОШИБКА!) "Забыли" вызвать free().
}

/**
* Текущая длина строки.
*/
int _string_length(const string_t* str) {
return str->sz;
}

/**
* Изменение длины строки.
*/
int _string_resize(string_t* str, int sz) {
return str->sz; // (ОШИБКА!) "Забыли" уменьшить длину строки.
}

/**
* Копирование строки из строки С, завершающейся нулем.
*/
string_t* _string_from_c_str(string_t* dst, const char* src) {
int sz = strlen(src);
memcpy(dst->data, src, sz);
// (ОШИБКА!) "Забыли" присвоить длине новое значение.
return dst;
}

/**
* Добавление символа в строку.
*/
string_t* _string_append_ch(string_t* str, char ch) {
str->data[str->sz] = ch; // (ОШИБКА!) "Забыли" увеличить длину.
return str;
}

/**
* Превращение строки в строку С с нулем на конце. Фактически,
* в тело строки добавляется ноль и возвращается указатель на данные.
*/
char* _string_c_str(string_t* str) {
// (ОШИБКА!) "Забыли" добавить 0 в конец.
return string_data(str);
}
Компилируем и запускаем:
string_declare_test: Starting test
string_declare_test: Test completed successfully.
string_c_str_test: Starting test
difference at offset 1 0x00 0x62
1 bytes of 0x0040f014 and 0x0012fe7c differ
ERROR: cstring_unittest.c:19 Failure!
string_c_str_test: Test failed.
string_append_ch_test: Starting test
"ab" != "b"
ERROR: cstring_unittest.c:39 Failure!
string_append_ch_test: Test failed.
string_heap_allocation_test: Starting test
Blocks allocated...
0x00326ee0 : cstring.c:27
ERROR: string_heap_allocation_test leaked 1 block(s)
string_heap_allocation_test: Test failed.
string_from_c_str_test: Starting test
Blocks allocated...
0x00326ee0 : cstring.c:27
Guard block of 0x00326f18 size=8 allocated by cstring.c:27 at 0x00326f20 is corrupt
ERROR: cmockery.c:1379 Failure!
string_from_c_str_test: Test failed.
string_resize_test: Starting test
0x1 != 0x2
ERROR: cstring_unittest.c:29 Failure!
string_resize_test: Test failed.
5 out of 6 tests failed!
string_c_str_test
string_append_ch_test
string_heap_allocation_test
string_from_c_str_test
string_resize_test
Blocks allocated...
0x00326ee0 : cstring.c:27
Guard block of 0x00326f18 size=8 allocated by cstring.c:27 at 0x00326f20 is corrupt
ERROR: cmockery.c:1379 Failure!
Бам! 5 из 6 тестов сломаны. Проанализируем полученное.

Тест string_c_str_test выявил, что функция string_c_str не добавила 0 в конец строки, хотя должна была:

string_c_str_test: Starting test
difference at offset 1 0x00 0x62
1 bytes of 0x0040f014 and 0x0012fe7c differ
ERROR: cstring_unittest.c:19 Failure!
string_c_str_test: Test failed.
Тест string_append_ch_test выявил, что функция добавления символа в конец строки не работает:
string_append_ch_test: Starting test
"ab" != "b"
ERROR: cstring_unittest.c:39 Failure!
string_append_ch_test: Test failed.
Тест string_heap_allocation_test выявил, что у нас имеется неосвобожденный блок памяти (утечка?). Конечно, мы же "забыли" освободить память в функции string_delete():
string_heap_allocation_test: Starting test
Blocks allocated...
0x00326ee0 : cstring.c:27
ERROR: string_heap_allocation_test leaked 1 block(s)
string_heap_allocation_test: Test failed.
Тест string_from_c_str_test выявил, что мы "вылезли" за границы выделенного куска памяти. Мы записали что-то мимо. Это болезненная ошибка. Конечно, cmockery не всегда может находить такие ляпы. Например, если переменная выделена с стеке, а не в куче, то проблема не вскроется. Тут уже помогут только динамические отладчики типа valgrind:
string_from_c_str_test: Starting test
Blocks allocated...
0x00326ee0 : cstring.c:27
Guard block of 0x00326f18 size=8 allocated by cstring.c:27 at 0x00326f20 is corrupt
ERROR: cmockery.c:1379 Failure!
string_from_c_str_test: Test failed.
Тест string_resize_test показал, что функция изменения размера строки не работает как положено:
string_resize_test: Starting test
0x1 != 0x2
ERROR: cstring_unittest.c:29 Failure!
string_resize_test: Test failed.
В целом, очень неплохие результаты.
Теперь представьте, что вы решили переписать реализацию библиотеки под новый процессор, чтобы работало в десять раз быстрее. Но как проверить результат? Элементарно. Запустите старые тесты. Если они работают, то по крайней мере с большой вероятностью вы не сломали старую функциональность. И, кстати, чем более тщательно написаны тесты, тем более ценны они. Чем более критична какая часть системы для стабильности системы в целом (например, библиотека строк или каких-то базовых контейнеров), тем более тщательно они должны быть покрыты тестами.
Конечно, уровень комфорта при написании тестов на С и их отладке очень далек даже от С++, но это не может быть оправданием для отказа от тестирования. Честно могу сказать, часто результатом работы "сломанного" теста в С, который неверно работает с памятью, например, может является просто зависание, а не красивый отчет, что тест "не работает". Но даже такой "знак" очень важен и дает понять, что что-то сломано. Пусть лучше повиснет тест, нежели готовый продукт у заказчика.

Под занавес приведу список основных функций-проверок (assert-фукнции), которые доступны в cmockery:

  • assert_true(), assert_false() — проверка булевых флагов
  • assert_int_equal(), assert_int_not_equal() — сравнение для типа int
  • assert_string_equal(), assert_string_not_equal() — сравнение для типа char* (для С-строк, заканчивающихся нулем)
  • assert_memory_equal(), assert_memory_not_equal() — сравнение кусков памяти
  • assert_in_range(), assert_not_in_range() — проверка нахождения числа в указанном интервале
  • assert_in_set(), assert_not_in_set() — проверка нахождения строки (char*) среди заданного набора строк
  • fail() — безусловное завершения теста с ошибкой
Вывод

Unit-тестирование в С порой сопряжено с трудностями, но оно возможно. И нет причин от него отказываться.