заставить Alchemy и UnitTest++ работать вместе

Я использую Adobe Alchemy в проекте, который использует UnitTest++. Модульные тесты запускаются как часть процесса сборки.

Оказывается, UnitTest++ зависит от функции C++, которая не реализована в Alchemy, а именно создание экземпляров статических классов и/или вызов функций для инициализации глобальных переменных.

Отличительной особенностью UnitTest++ является то, что вам не нужно помнить о добавлении ваших тестов в список тестов для запуска. Это происходит автоматически с помощью макромагии для создания тестовых классов и добавления их в глобальный список тестов. Итак, это:

TEST(MyTest) {
    CHECK(doSomething());
}

становится таким:

class TestMyTest : public UnitTest::Test {
   ...
} testMyTestInstance;

UnitTest::ListAdder adderMyTest(UnitTest::Test::GetTestList(), &testMyTestInstance);

где конструктор для ListAdder добавляет testMyTestInstance в глобальный список тестов.

Проблема в том, что из-за бага Alchemy конструктор ListAdder никогда не запускается, поэтому список тестов всегда пуст.

Чтобы доказать, что конструктор ListAdder никогда не вызывается, вы можете настроить его так, чтобы он аварийно завершал работу при вызове:

ListAdder::ListAdder(TestList& list, Test* test) {
    int *p= (int*)INT_MAX;   // NULL won't crash alchemy (!)
    *p= 0;                   // boom
    list.Add(test);
}

Это приведет к сбою при исходной компиляции, но не при компиляции с помощью Alchemy.

Менее радикальный способ увидеть это — просто добавить printf:

ListAdder::ListAdder(TestList& list, Test* test) {
    printf("ListAdder %s \n", test->m_details.testName);
    list.Add(test);
}

При исходной компиляции вы увидите "ListAdder..." для каждого теста, но при компиляции под Alchemy ничего не печатается.

Мой вопрос: как я могу изменить UnitTest++, чтобы тесты запускались? Обходные пути, описанные здесь, похоже, не применяются.


person paleozogt    schedule 17.11.2010    source источник


Ответы (1)


Пришлось немного повозиться, но я разобрался. Хитрость в том, что статические инициализаторы функции будут работать, например,

int someFunc() {
    return 42;
}
int someVal= someFunc();

пока они не вызывают никаких конструкторов, не используют new/malloc или не используют printf. (Мне потребовалось некоторое время, чтобы понять, что Gunslinger47 был прав насчет того, что printfs все портит.)

Тот факт, что статические функции инициализации работают, достаточен, чтобы заставить работать UnitTest++. Что мы делаем, так это используем вариант обходного пути «Указатели», описанный здесь:

  • Вместо статического распределения каждый тестовый класс имеет функцию распределения.
  • Указатель на каждую функцию-распределитель добавляется в список указателей на функции.
  • В main этот список указателей функций затем повторяется и вызывается каждая функция.

Подробности ниже:

(1) В TestMacros.h измените макрос TEST_EX, чтобы использовать статическую функцию инициализации, а не конструктор:

#define TEST_EX(Name, List)                                                \
    class Test##Name : public UnitTest::Test                               \
    {                                                                      \
    public:                                                                \
        Test##Name() : Test(#Name, UnitTestSuite::GetSuiteName(), __FILE__, __LINE__) {}  \
    private:                                                               \
        virtual void RunImpl() const;                                      \
    };                                                                     \
                                                                           \
    void create_test##Name##Instance() {                                   \
        Test##Name *test##Name##Instance= new Test##Name();                \
        UnitTest::ListAdder adder##Name (List(), test##Name##Instance);    \
    }                                                                      \
                                                                           \
    UnitTest::test_creator_func_t fp_create_test##Name##Instance=          \
                    UnitTest::addTestCreator(create_test##Name##Instance); \
                                                                           \
    void Test##Name::RunImpl() const


#define TEST(Name) TEST_EX(Name, UnitTest::Test::GetTestList)

(2) Измените TEST_FIXTURE_EX аналогично TEST_EX. Я избавлю вас от многословия.

(3) Внизу TestList.cpp добавьте функции, которые вызывают макросы TEST_EX/TEST_FIXTURE_EX:

#if !defined(MAX_TEST_CREATORS)
#define MAX_TEST_CREATORS 1024
#endif

const size_t max_test_creators= MAX_TEST_CREATORS;
size_t num_test_creators= 0;

// This list unfortunately must be static-- if we were to 
// dynamically allocate it, then alchemy would break.
// If it winds up not being big enough, then just inject
// a bigger definition for MAX_TEST_CREATORS 
test_creator_func_t test_creator_list[max_test_creators]= {NULL};   

test_creator_func_t addTestCreator(test_creator_func_t fp) {
    int idx= num_test_creators;

    num_test_creators++;    
    if (num_test_creators > max_test_creators) {
        throw "test creator overflow";
    }

    test_creator_list[idx]= fp;
    return fp;
}

void initializeAllTests() {
    for (size_t idx= 0; idx < num_test_creators; idx++) {
        test_creator_list[idx]();
    }
}

и конечно добавить их прототипы в TestList.h:

typedef void (*test_creator_func_t)();
test_creator_func_t addTestCreator(test_creator_func_t fp);
void initializeAllTests();

(4) Наконец, в вашем модуле запуска модульных тестов вы должны вызвать initializeAllTests:

UnitTest::initializeAllTests();
return UnitTest::RunAllTests();

Но это не все! Есть несколько других лакомых кусочков, которые необходимо сделать, прежде чем он заработает:

(1) Убедитесь, что UNITTEST_USE_CUSTOM_STREAMS определен в Config.h:

// by default, MemoryOutStream is implemented in terms of std::ostringstream, which can be expensive.
// uncomment this line to use the custom MemoryOutStream (no deps on std::ostringstream).

#define UNITTEST_USE_CUSTOM_STREAMS

Причина этого в том, что если он не определен, MemoryOutStream.h будет #include <sstream>, что нарушит статическую инициализацию (я подозреваю, что он выполняет какой-то глобальный конструктор или что-то в этом роде).

(2) В SignalTranslator.h убедитесь, что макрос UNITTEST_THROW_SIGNALS является нулевым. Я делаю это, вводя -D__ALCHEMY__ в свои сборки и проверяя его:

#if defined(__ALCHEMY__)
#define UNITTEST_THROW_SIGNALS
#else
#define UNITTEST_THROW_SIGNALS \
    UnitTest::SignalTranslator sig; \
    if (UNITTEST_EXTENSION sigsetjmp(*UnitTest::SignalTranslator::s_jumpTarget, 1) != 0) \
        throw ("Unhandled system exception"); 
#endif

Если этого не сделать, вызов sigsetjmp завершится ошибкой во время выполнения.

person paleozogt    schedule 30.11.2010