summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--indra/llcommon/llsingleton.cpp36
-rw-r--r--indra/llcommon/llsingleton.h334
-rw-r--r--indra/llcommon/tests/llsingleton_test.cpp14
3 files changed, 218 insertions, 166 deletions
diff --git a/indra/llcommon/llsingleton.cpp b/indra/llcommon/llsingleton.cpp
index 812fd31719..bf594f122c 100644
--- a/indra/llcommon/llsingleton.cpp
+++ b/indra/llcommon/llsingleton.cpp
@@ -302,7 +302,7 @@ void LLSingletonBase::MasterList::LockedInitializing::log(const char* verb, cons
}
}
-void LLSingletonBase::capture_dependency(EInitState initState)
+void LLSingletonBase::capture_dependency()
{
MasterList::LockedInitializing locked_list;
list_t& initializing(locked_list.get());
@@ -334,21 +334,8 @@ void LLSingletonBase::capture_dependency(EInitState initState)
LLSingletonBase* foundp(*found);
out << classname(foundp) << " -> ";
}
- // We promise to capture dependencies from both the constructor
- // and the initSingleton() method, so an LLSingleton's instance
- // pointer is on the initializing list during both. Now that we've
- // detected circularity, though, we must distinguish the two. If
- // the recursive call is from the constructor, we CAN'T honor it:
- // otherwise we'd be returning a pointer to a partially-
- // constructed object! But from initSingleton() is okay: that
- // method exists specifically to support circularity.
// Decide which log helper to call.
- if (initState == CONSTRUCTING)
- {
- logerrs("LLSingleton circularity in Constructor: ", out.str().c_str(),
- classname(this).c_str(), "");
- }
- else if (it_next == initializing.end())
+ if (it_next == initializing.end())
{
// Points to self after construction, but during initialization.
// Singletons can initialize other classes that depend onto them,
@@ -457,6 +444,19 @@ void LLSingletonBase::cleanupAll()
}
}
+void LLSingletonBase::cleanup_()
+{
+ logdebugs("calling ", classname(this).c_str(), "::cleanupSingleton()");
+ try
+ {
+ cleanupSingleton();
+ }
+ catch (...)
+ {
+ LOG_UNHANDLED_EXCEPTION(classname(this) + "::cleanupSingleton()");
+ }
+}
+
//static
void LLSingletonBase::deleteAll()
{
@@ -537,6 +537,12 @@ void LLSingletonBase::logwarns(const char* p1, const char* p2, const char* p3, c
}
//static
+void LLSingletonBase::loginfos(const char* p1, const char* p2, const char* p3, const char* p4)
+{
+ log(LLError::LEVEL_INFO, p1, p2, p3, p4);
+}
+
+//static
void LLSingletonBase::logerrs(const char* p1, const char* p2, const char* p3, const char* p4)
{
log(LLError::LEVEL_ERROR, p1, p2, p3, p4);
diff --git a/indra/llcommon/llsingleton.h b/indra/llcommon/llsingleton.h
index ebae601029..4f3b8ceb38 100644
--- a/indra/llcommon/llsingleton.h
+++ b/indra/llcommon/llsingleton.h
@@ -31,6 +31,9 @@
#include <vector>
#include <typeinfo>
#include "mutex.h"
+#include "lockstatic.h"
+#include "llthread.h" // on_main_thread()
+#include "llmainthreadtask.h"
class LLSingletonBase: private boost::noncopyable
{
@@ -105,7 +108,7 @@ protected:
protected:
// If a given call to B::getInstance() happens during either A::A() or
// A::initSingleton(), record that A directly depends on B.
- void capture_dependency(EInitState);
+ void capture_dependency();
// delegate LL_ERRS() logging to llsingleton.cpp
static void logerrs(const char* p1, const char* p2="",
@@ -113,6 +116,9 @@ protected:
// delegate LL_WARNS() logging to llsingleton.cpp
static void logwarns(const char* p1, const char* p2="",
const char* p3="", const char* p4="");
+ // delegate LL_INFOS() logging to llsingleton.cpp
+ static void loginfos(const char* p1, const char* p2="",
+ const char* p3="", const char* p4="");
static std::string demangle(const char* mangled);
template <typename T>
static std::string classname() { return demangle(typeid(T).name()); }
@@ -123,6 +129,9 @@ protected:
virtual void initSingleton() {}
virtual void cleanupSingleton() {}
+ // internal wrapper around calls to cleanupSingleton()
+ void cleanup_();
+
// deleteSingleton() isn't -- and shouldn't be -- a virtual method. It's a
// class static. However, given only Foo*, deleteAll() does need to be
// able to reach Foo::deleteSingleton(). Make LLSingleton (which declares
@@ -193,9 +202,9 @@ struct LLSingleton_manage_master
{
return LLSingletonBase::get_initializing_size();
}
- void capture_dependency(LLSingletonBase* sb, LLSingletonBase::EInitState state)
+ void capture_dependency(LLSingletonBase* sb)
{
- sb->capture_dependency(state);
+ sb->capture_dependency();
}
};
@@ -210,14 +219,14 @@ struct LLSingleton_manage_master<LLSingletonBase::MasterList>
// since we never pushed, no need to clean up
void reset_initializing(LLSingletonBase::list_t::size_type size) {}
LLSingletonBase::list_t::size_type get_initializing_size() { return 0; }
- void capture_dependency(LLSingletonBase*, LLSingletonBase::EInitState) {}
+ void capture_dependency(LLSingletonBase*) {}
};
// Now we can implement LLSingletonBase's template constructor.
template <typename DERIVED_TYPE>
LLSingletonBase::LLSingletonBase(tag<DERIVED_TYPE>):
mCleaned(false),
- mDeleteSingleton(NULL)
+ mDeleteSingleton(nullptr)
{
// This is the earliest possible point at which we can push this new
// instance onto the init stack. LLSingleton::constructSingleton() can't
@@ -295,65 +304,40 @@ template <typename DERIVED_TYPE>
class LLSingleton : public LLSingletonBase
{
private:
- // Allow LLParamSingleton subclass -- but NOT DERIVED_TYPE itself -- to
- // access our private members.
- friend class LLParamSingleton<DERIVED_TYPE>;
-
- // Scoped lock on the mutex associated with this LLSingleton<T>
- class Locker
+ // LLSingleton<DERIVED_TYPE> must have a distinct instance of
+ // SingletonData for every distinct DERIVED_TYPE. It's tempting to
+ // consider hoisting SingletonData up into LLSingletonBase. Don't do it.
+ struct SingletonData
{
- public:
- Locker(): mLock(getMutex()) {}
-
- private:
// Use a recursive_mutex in case of constructor circularity. With a
// non-recursive mutex, that would result in deadlock.
typedef std::recursive_mutex mutex_t;
+ mutex_t mMutex; // LockStatic looks for mMutex
- // LLSingleton<T> must have a distinct instance of sMutex for every
- // distinct T. It's tempting to consider hoisting Locker up into
- // LLSingletonBase. Don't do it.
- //
- // sMutex must be a function-local static rather than a static member. One
- // of the essential features of LLSingleton and friends is that they must
- // support getInstance() even when the containing module's static
- // variables have not yet been runtime-initialized. A mutex requires
- // construction. A static class member might not yet have been
- // constructed.
- //
- // We could store a dumb mutex_t*, notice when it's NULL and allocate a
- // heap mutex -- but that's vulnerable to race conditions. And we can't
- // defend the dumb pointer with another mutex.
- //
- // We could store a std::atomic<mutex_t*> -- but a default-constructed
- // std::atomic<T> does not contain a valid T, even a default-constructed
- // T! Which means std::atomic, too, requires runtime initialization.
- //
- // But a function-local static is guaranteed to be initialized exactly
- // once, the first time control reaches that declaration.
- static mutex_t& getMutex()
- {
- static mutex_t sMutex;
- return sMutex;
- }
-
- std::unique_lock<mutex_t> mLock;
+ EInitState mInitState{UNINITIALIZED};
+ DERIVED_TYPE* mInstance{nullptr};
};
+ typedef llthread::LockStatic<SingletonData> LockStatic;
+
+ // Allow LLParamSingleton subclass -- but NOT DERIVED_TYPE itself -- to
+ // access our private members.
+ friend class LLParamSingleton<DERIVED_TYPE>;
// LLSingleton only supports a nullary constructor. However, the specific
// purpose for its subclass LLParamSingleton is to support Singletons
// requiring constructor arguments. constructSingleton() supports both use
// cases.
+ // Accepting LockStatic& requires that the caller has already locked our
+ // static data before calling.
template <typename... Args>
- static void constructSingleton(Args&&... args)
+ static void constructSingleton(LockStatic& lk, Args&&... args)
{
auto prev_size = LLSingleton_manage_master<DERIVED_TYPE>().get_initializing_size();
- // getInstance() calls are from within constructor
- sData.mInitState = CONSTRUCTING;
+ // Any getInstance() calls after this point are from within constructor
+ lk->mInitState = CONSTRUCTING;
try
{
- sData.mInstance = new DERIVED_TYPE(std::forward<Args>(args)...);
- // we have called constructor, have not yet called initSingleton()
+ lk->mInstance = new DERIVED_TYPE(std::forward<Args>(args)...);
}
catch (const std::exception& err)
{
@@ -367,55 +351,56 @@ private:
// There isn't a separate EInitState value meaning "we attempted
// to construct this LLSingleton subclass but could not," so use
// DELETED. That seems slightly more appropriate than UNINITIALIZED.
- sData.mInitState = DELETED;
+ lk->mInitState = DELETED;
// propagate the exception
throw;
}
- // getInstance() calls are from within initSingleton()
- sData.mInitState = INITIALIZING;
+ // Any getInstance() calls after this point are from within initSingleton()
+ lk->mInitState = INITIALIZING;
try
{
// initialize singleton after constructing it so that it can
// reference other singletons which in turn depend on it, thus
// breaking cyclic dependencies
- sData.mInstance->initSingleton();
- sData.mInitState = INITIALIZED;
+ lk->mInstance->initSingleton();
+ lk->mInitState = INITIALIZED;
// pop this off stack of initializing singletons
- pop_initializing();
+ pop_initializing(lk->mInstance);
}
catch (const std::exception& err)
{
// pop this off stack of initializing singletons here, too --
// BEFORE logging, so log-machinery LLSingletons don't record a
// dependency on DERIVED_TYPE!
- pop_initializing();
+ pop_initializing(lk->mInstance);
logwarns("Error in ", classname<DERIVED_TYPE>().c_str(),
"::initSingleton(): ", err.what());
- // and get rid of the instance entirely
+ // Get rid of the instance entirely. This call depends on our
+ // recursive_mutex. We could have a deleteSingleton(LockStatic&)
+ // overload and pass lk, but we don't strictly need it.
deleteSingleton();
// propagate the exception
throw;
}
}
- static void pop_initializing()
+ static void pop_initializing(LLSingletonBase* sb)
{
// route through LLSingleton_manage_master so we Do The Right Thing
// (namely, nothing) for MasterList
- LLSingleton_manage_master<DERIVED_TYPE>().pop_initializing(sData.mInstance);
+ LLSingleton_manage_master<DERIVED_TYPE>().pop_initializing(sb);
}
- static void capture_dependency()
+ static void capture_dependency(LLSingletonBase* sb)
{
// By this point, if DERIVED_TYPE was pushed onto the initializing
// stack, it has been popped off. So the top of that stack, if any, is
// an LLSingleton that directly depends on DERIVED_TYPE. If
// getInstance() was called by another LLSingleton, rather than from
// vanilla application code, record the dependency.
- LLSingleton_manage_master<DERIVED_TYPE>().capture_dependency(
- sData.mInstance, sData.mInitState);
+ LLSingleton_manage_master<DERIVED_TYPE>().capture_dependency(sb);
}
// We know of no way to instruct the compiler that every subclass
@@ -442,19 +427,6 @@ protected:
LLSingleton_manage_master<DERIVED_TYPE>().add(this);
}
-protected:
- virtual ~LLSingleton()
- {
- // In case racing threads call getInstance() at the same moment as
- // this destructor, serialize the calls.
- Locker lk;
-
- // remove this instance from the master list
- LLSingleton_manage_master<DERIVED_TYPE>().remove(this);
- sData.mInstance = NULL;
- sData.mInitState = DELETED;
- }
-
public:
/**
* @brief Immediately delete the singleton.
@@ -479,54 +451,149 @@ public:
*/
static void deleteSingleton()
{
- delete sData.mInstance;
- // SingletonData state handled by destructor, above
+ DERIVED_TYPE* lameduck;
+ {
+ LockStatic lk;
+ // Capture the instance and clear SingletonData. This sequence
+ // guards against the chance that the destructor throws, somebody
+ // catches it and there's a subsequent call to getInstance().
+ lameduck = lk->mInstance;
+ lk->mInstance = nullptr;
+ lk->mInitState = DELETED;
+ // At this point we can safely unlock SingletonData during the
+ // remaining cleanup. If another thread calls deleteSingleton() (or
+ // getInstance(), or whatever) it won't find our instance, now
+ // referenced only as 'lameduck'.
+ }
+ // of course, only cleanup and delete if there's something there
+ if (lameduck)
+ {
+ // remove this instance from the master list BEFORE attempting
+ // cleanup so possible destructor exception won't leave the master
+ // list confused
+ LLSingleton_manage_master<DERIVED_TYPE>().remove(lameduck);
+ lameduck->cleanup_();
+ delete lameduck;
+ }
}
static DERIVED_TYPE* getInstance()
{
- // In case racing threads call getInstance() at the same moment,
- // serialize the calls.
- Locker lk;
-
- switch (sData.mInitState)
- {
- case CONSTRUCTING:
- // here if DERIVED_TYPE's constructor (directly or indirectly)
- // calls DERIVED_TYPE::getInstance()
- logerrs("Tried to access singleton ",
- classname<DERIVED_TYPE>().c_str(),
- " from singleton constructor!");
- return NULL;
-
- case UNINITIALIZED:
- constructSingleton();
- break;
-
- case INITIALIZING:
- // here if DERIVED_TYPE::initSingleton() (directly or indirectly)
- // calls DERIVED_TYPE::getInstance(): go ahead and allow it
- case INITIALIZED:
- // normal subsequent calls
- break;
-
- case DELETED:
- // called after deleteSingleton()
- logwarns("Trying to access deleted singleton ",
- classname<DERIVED_TYPE>().c_str(),
- " -- creating new instance");
- constructSingleton();
- break;
- }
-
- // record the dependency, if any: check if we got here from another
- // LLSingleton's constructor or initSingleton() method
- capture_dependency();
- return sData.mInstance;
+ // We know the viewer has LLSingleton dependency circularities. If you
+ // feel strongly motivated to eliminate them, cheers and good luck.
+ // (At that point we could consider a much simpler locking mechanism.)
+
+ // If A and B depend on each other, and thread T1 requests A at the
+ // same moment thread T2 requests B, you could get a sequence like this:
+ // - T1 locks A
+ // - T2 locks B
+ // - T1, having constructed A, calls A::initSingleton(), which calls
+ // B::getInstance() and blocks on B's lock
+ // - T2, having constructed B, calls B::initSingleton(), which calls
+ // A::getInstance() and blocks on A's lock
+ // In other words, classic deadlock.
+
+ // Avoid that by constructing and initializing every LLSingleton on
+ // the main thread. In that scenario:
+ // - T1 locks A
+ // - T2 locks B
+ // - T1 discovers A is UNINITIALIZED, so it queues a task for the main
+ // thread, unlocks A and blocks on the std::future.
+ // - T2 discovers B is UNINITIALIZED, so it queues a task for the main
+ // thread, unlocks B and blocks on the std::future.
+ // - The main thread executes T1's request for A. It locks A and
+ // starts to construct it.
+ // - A::initSingleton() calls B::getInstance(). Fine: nobody's holding
+ // B's lock.
+ // - The main thread locks B, constructs B, calls B::initSingleton(),
+ // which calls A::getInstance(), which returns A.
+ // - B::getInstance() returns B to A::initSingleton(), unlocking B.
+ // - A::getInstance() returns A to the task wrapper, unlocking A.
+ // - The task wrapper passes A to T1 via the future. T1 resumes.
+ // - The main thread executes T2's request for B. Oh look, B already
+ // exists. The task wrapper passes B to T2 via the future. T2
+ // resumes.
+ // This still works even if one of T1 or T2 *is* the main thread.
+ // This still works even if thread T3 requests B at the same moment as
+ // T2. Finding B still UNINITIALIZED, T3 also queues a task for the
+ // main thread, unlocks B and blocks on a (distinct) std::future. By
+ // the time the main thread executes T3's request for B, B already
+ // exists, and is simply delivered via the future.
+
+ { // nested scope for 'lk'
+ // In case racing threads call getInstance() at the same moment,
+ // serialize the calls.
+ LockStatic lk;
+
+ switch (lk->mInitState)
+ {
+ case CONSTRUCTING:
+ // here if DERIVED_TYPE's constructor (directly or indirectly)
+ // calls DERIVED_TYPE::getInstance()
+ logerrs("Tried to access singleton ",
+ classname<DERIVED_TYPE>().c_str(),
+ " from singleton constructor!");
+ return nullptr;
+
+ case INITIALIZING:
+ // here if DERIVED_TYPE::initSingleton() (directly or indirectly)
+ // calls DERIVED_TYPE::getInstance(): go ahead and allow it
+ case INITIALIZED:
+ // normal subsequent calls
+ // record the dependency, if any: check if we got here from another
+ // LLSingleton's constructor or initSingleton() method
+ capture_dependency(lk->mInstance);
+ return lk->mInstance;
+
+ case DELETED:
+ // called after deleteSingleton()
+ logwarns("Trying to access deleted singleton ",
+ classname<DERIVED_TYPE>().c_str(),
+ " -- creating new instance");
+ // fall through
+ case UNINITIALIZED:
+ break;
+ }
+
+ // Here we need to construct a new instance.
+ if (on_main_thread())
+ {
+ // On the main thread, directly construct the instance while
+ // holding the lock.
+ constructSingleton(lk);
+ capture_dependency(lk->mInstance);
+ return lk->mInstance;
+ }
+ } // unlock 'lk'
+
+ // Here we need to construct a new instance, but we're on a secondary
+ // thread. Per the comment block above, dispatch to the main thread.
+ loginfos(classname<DERIVED_TYPE>().c_str(),
+ "::getInstance() dispatching to main thread");
+ auto instance = LLMainThreadTask::dispatch(
+ [](){
+ // VERY IMPORTANT to call getInstance() on the main thread,
+ // rather than going straight to constructSingleton()!
+ // During the time window before mInitState is INITIALIZED,
+ // multiple requests might be queued. It's essential that, as
+ // the main thread processes them, only the FIRST such request
+ // actually constructs the instance -- every subsequent one
+ // simply returns the existing instance.
+ loginfos(classname<DERIVED_TYPE>().c_str(),
+ "::getInstance() on main thread");
+ return getInstance();
+ });
+ // record the dependency chain tracked on THIS thread, not the main
+ // thread (consider a getInstance() overload with a tag param that
+ // suppresses dep tracking when dispatched to the main thread)
+ capture_dependency(instance);
+ loginfos(classname<DERIVED_TYPE>().c_str(),
+ "::getInstance() returning on invoking thread");
+ return instance;
}
// Reference version of getInstance()
- // Preferred over getInstance() as it disallows checking for NULL
+ // Preferred over getInstance() as it disallows checking for nullptr
static DERIVED_TYPE& instance()
{
return *getInstance();
@@ -537,8 +604,8 @@ public:
static bool instanceExists()
{
// defend any access to sData from racing threads
- Locker lk;
- return sData.mInitState == INITIALIZED;
+ LockStatic lk;
+ return lk->mInitState == INITIALIZED;
}
// Has this singleton been deleted? This can be useful during shutdown
@@ -547,24 +614,11 @@ public:
static bool wasDeleted()
{
// defend any access to sData from racing threads
- Locker lk;
- return sData.mInitState == DELETED;
+ LockStatic lk;
+ return lk->mInitState == DELETED;
}
-
-private:
- struct SingletonData
- {
- // explicitly has a default constructor so that member variables are zero initialized in BSS
- // and only changed by singleton logic, not constructor running during startup
- EInitState mInitState;
- DERIVED_TYPE* mInstance;
- };
- static SingletonData sData;
};
-template<typename T>
-typename LLSingleton<T>::SingletonData LLSingleton<T>::sData;
-
/**
* LLParamSingleton<T> is like LLSingleton<T>, except in the following ways:
@@ -589,7 +643,7 @@ class LLParamSingleton : public LLSingleton<DERIVED_TYPE>
{
private:
typedef LLSingleton<DERIVED_TYPE> super;
- using typename super::Locker;
+ using typename super::LockStatic;
public:
using super::deleteSingleton;
@@ -603,9 +657,9 @@ public:
// In case racing threads both call initParamSingleton() at the same
// time, serialize them. One should initialize; the other should see
// mInitState already set.
- Locker lk;
+ LockStatic lk;
// For organizational purposes this function shouldn't be called twice
- if (super::sData.mInitState != super::UNINITIALIZED)
+ if (lk->mInitState != super::UNINITIALIZED)
{
super::logerrs("Tried to initialize singleton ",
super::template classname<DERIVED_TYPE>().c_str(),
@@ -613,7 +667,7 @@ public:
}
else
{
- super::constructSingleton(std::forward<Args>(args)...);
+ super::constructSingleton(lk, std::forward<Args>(args)...);
}
}
@@ -621,9 +675,9 @@ public:
{
// In case racing threads call getInstance() at the same moment as
// initParamSingleton(), serialize the calls.
- Locker lk;
+ LockStatic lk;
- switch (super::sData.mInitState)
+ switch (lk->mInitState)
{
case super::UNINITIALIZED:
super::logerrs("Uninitialized param singleton ",
@@ -641,8 +695,8 @@ public:
// within initSingleton()
case super::INITIALIZED:
// for any valid call, capture dependencies
- super::capture_dependency();
- return super::sData.mInstance;
+ super::capture_dependency(lk->mInstance);
+ return lk->mInstance;
case super::DELETED:
super::logerrs("Trying to access deleted param singleton ",
diff --git a/indra/llcommon/tests/llsingleton_test.cpp b/indra/llcommon/tests/llsingleton_test.cpp
index 75ddff9d7d..15ffe68e67 100644
--- a/indra/llcommon/tests/llsingleton_test.cpp
+++ b/indra/llcommon/tests/llsingleton_test.cpp
@@ -143,8 +143,6 @@ namespace tut
\
(void)CLS::instance(); \
ensure_equals(sLog, #CLS "i" #CLS); \
- LLSingletonBase::cleanupAll(); \
- ensure_equals(sLog, #CLS "i" #CLS "x" #CLS); \
LLSingletonBase::deleteAll(); \
ensure_equals(sLog, #CLS "i" #CLS "x" #CLS "~" #CLS); \
} \
@@ -159,10 +157,8 @@ namespace tut
\
(void)CLS::instance(); \
ensure_equals(sLog, #CLS #OTHER "i" #OTHER "i" #CLS); \
- LLSingletonBase::cleanupAll(); \
- ensure_equals(sLog, #CLS #OTHER "i" #OTHER "i" #CLS "x" #CLS "x" #OTHER); \
LLSingletonBase::deleteAll(); \
- ensure_equals(sLog, #CLS #OTHER "i" #OTHER "i" #CLS "x" #CLS "x" #OTHER "~" #CLS "~" #OTHER); \
+ ensure_equals(sLog, #CLS #OTHER "i" #OTHER "i" #CLS "x" #CLS "~" #CLS "x" #OTHER "~" #OTHER); \
} \
\
template<> template<> \
@@ -175,10 +171,8 @@ namespace tut
\
(void)CLS::instance(); \
ensure_equals(sLog, #CLS "i" #CLS #OTHER "i" #OTHER); \
- LLSingletonBase::cleanupAll(); \
- ensure_equals(sLog, #CLS "i" #CLS #OTHER "i" #OTHER "x" #CLS "x" #OTHER); \
LLSingletonBase::deleteAll(); \
- ensure_equals(sLog, #CLS "i" #CLS #OTHER "i" #OTHER "x" #CLS "x" #OTHER "~" #CLS "~" #OTHER); \
+ ensure_equals(sLog, #CLS "i" #CLS #OTHER "i" #OTHER "x" #CLS "~" #CLS "x" #OTHER "~" #OTHER); \
} \
\
template<> template<> \
@@ -191,10 +185,8 @@ namespace tut
\
(void)CLS::instance(); \
ensure_equals(sLog, #CLS "i" #CLS #OTHER "i" #OTHER); \
- LLSingletonBase::cleanupAll(); \
- ensure_equals(sLog, #CLS "i" #CLS #OTHER "i" #OTHER "x" #CLS "x" #OTHER); \
LLSingletonBase::deleteAll(); \
- ensure_equals(sLog, #CLS "i" #CLS #OTHER "i" #OTHER "x" #CLS "x" #OTHER "~" #CLS "~" #OTHER); \
+ ensure_equals(sLog, #CLS "i" #CLS #OTHER "i" #OTHER "x" #CLS "~" #CLS "x" #OTHER "~" #OTHER); \
}
TESTS(A, B, 4, 5, 6, 7)