Question

I'm using Adobe Alchemy in a project that uses UnitTest++. The unit tests run as part of the build process.

It turns out that UnitTest++ depends on a feature of C++ that isn't implemented in Alchemy, namely instantiating static classes and/or calling functions to initialize global vars.

The great thing about UnitTest++ is that you don't have to remember to add your tests to the list of tests to run. It happens automatically using some macro-magic to create test-case classes and add them to a global list of tests. So this:

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

becomes this:

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

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

where the constructor for ListAdder adds testMyTestInstance to the global list of tests.

The problem is that because of the Alchemy bug, the ListAdder constructor never runs, so the list of tests is always empty.

To prove that the ListAdder constructor is never getting called, you can instrument it to crash when it gets called:

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

This will crash when compiled natively, but won't when compiled with Alchemy.

A less drastic way to see it is to just add a printf:

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

When compiled natively, you'll see "ListAdder ..." for each test, but when compiled under Alchemy it won't print anything.

My question is: how can I modify UnitTest++ so that the tests will run? The workarounds described here don't seem to apply.

Was it helpful?

Solution

It took some doing, but I figured it out. The trick is that static initializer functions will work, e.g.,

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

as long as they don't call any constructors or use new/malloc or use printf. (It took me a while to realize that Gunslinger47 was right about the printfs screwing things up.)

The fact that static initializer functions work is just enough to let us get UnitTest++ working. What we do is use a variation of the "Pointers" workaround described here:

  • Instead of being allocated statically, each test class has an allocator function
  • A pointer to each allocator function is added to a list of function pointers
  • In main, this list of function pointers is then iterated through and each function called.

The gritty details are below:

(1) In TestMacros.h, change the TEST_EX macro to use a static initializer function rather than a constructor:

#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) Change TEST_FIXTURE_EX in a similar manner as TEST_EX. I'll spare you the verbosity.

(3) At the bottom of TestList.cpp, add the functions that the TEST_EX/TEST_FIXTURE_EX macros call:

#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]();
    }
}

and of course add their prototypes to TestList.h:

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

(4) Finally, in your unit test runner, you must call initializeAllTests:

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

But that's not all! There are a few other tidbits that need to be done before it'll work:

(1) Make sure UNITTEST_USE_CUSTOM_STREAMS is defined in 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

The reason for this is that if its not defined, MemoryOutStream.h will #include <sstream>, which will break static initialization (I suspect that it does some kind of global constructor or something).

(2) In SignalTranslator.h, make sure that the UNITTEST_THROW_SIGNALS macro is a noop. I do this by injecting a -D__ALCHEMY__ into my builds and checking for it:

#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

If this isn't done, the sigsetjmp call will fail at runtime.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top