diff options
Diffstat (limited to 'indra/test')
| -rw-r--r-- | indra/test/CMakeLists.txt | 4 | ||||
| -rw-r--r-- | indra/test/chained_callback.h | 107 | ||||
| -rw-r--r-- | indra/test/debug.h | 42 | ||||
| -rw-r--r-- | indra/test/llevents_tut.cpp | 303 | ||||
| -rw-r--r-- | indra/test/lltestapp.h | 34 | ||||
| -rw-r--r-- | indra/test/print.h | 42 | ||||
| -rw-r--r-- | indra/test/setenv.h | 66 | ||||
| -rw-r--r-- | indra/test/sync.h | 116 | ||||
| -rw-r--r-- | indra/test/test.cpp | 98 | 
9 files changed, 502 insertions, 310 deletions
| diff --git a/indra/test/CMakeLists.txt b/indra/test/CMakeLists.txt index 8344cead57..87536e146b 100644 --- a/indra/test/CMakeLists.txt +++ b/indra/test/CMakeLists.txt @@ -67,6 +67,7 @@ set(test_HEADER_FILES      llpipeutil.h      llsdtraits.h      lltut.h +    sync.h      )  if (NOT WINDOWS) @@ -83,6 +84,7 @@ list(APPEND test_SOURCE_FILES ${test_HEADER_FILES})  add_executable(lltest ${test_SOURCE_FILES})  target_link_libraries(lltest +    ${LEGACY_STDIO_LIBS}      ${LLDATABASE_LIBRARIES}      ${LLINVENTORY_LIBRARIES}      ${LLMESSAGE_LIBRARIES} @@ -98,7 +100,7 @@ target_link_libraries(lltest      ${WINDOWS_LIBRARIES}      ${BOOST_PROGRAM_OPTIONS_LIBRARY}      ${BOOST_REGEX_LIBRARY} -    ${BOOST_COROUTINE_LIBRARY} +    ${BOOST_FIBER_LIBRARY}      ${BOOST_CONTEXT_LIBRARY}      ${BOOST_SYSTEM_LIBRARY}      ${DL_LIBRARY} diff --git a/indra/test/chained_callback.h b/indra/test/chained_callback.h new file mode 100644 index 0000000000..05929e33ad --- /dev/null +++ b/indra/test/chained_callback.h @@ -0,0 +1,107 @@ +/** + * @file   chained_callback.h + * @author Nat Goodspeed + * @date   2020-01-03 + * @brief  Subclass of tut::callback used for chaining callbacks. + *  + * $LicenseInfo:firstyear=2020&license=viewerlgpl$ + * Copyright (c) 2020, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_CHAINED_CALLBACK_H) +#define LL_CHAINED_CALLBACK_H + +#include "lltut.h" + +/** + * Derive your TUT callback from chained_callback instead of tut::callback to + * ensure that multiple such callbacks can coexist in a given test executable. + * The relevant callback method will be called for each callback instance in + * reverse order of the instance's link() methods being called: the most + * recently link()ed callback will be called first, then the previous, and so + * forth. + * + * Obviously, for this to work, all relevant callbacks must be derived from + * chained_callback instead of tut::callback. Given that, control should reach + * each of them regardless of their construction order. The chain is + * guaranteed to stop because the first link() call will link to test_runner's + * default_callback, which is simply an instance of the callback() base class. + * + * The rule for deriving from chained_callback is that you may override any of + * its virtual methods, but your override must at some point call the + * corresponding chained_callback method. + */ +class chained_callback: public tut::callback +{ +public: +    /** +     * Instead of calling tut::test_runner::set_callback(&your_callback), call +     * your_callback.link(); +     * This uses the canonical instance of tut::test_runner. +     */ +    void link() +    { +        link(tut::runner.get()); +    } + +    /** +     * If for some reason you have a different instance of test_runner... +     */ +    void link(tut::test_runner& runner) +    { +        // Since test_runner's constructor sets a default callback, +        // get_callback() will always return a reference to a valid callback +        // instance. +        mPrev = &runner.get_callback(); +        runner.set_callback(this); +    } + +    /** +     * Called when new test run started. +     */ +    virtual void run_started() +    { +        mPrev->run_started(); +    } + +    /** +     * Called when a group started +     * @param name Name of the group +     */ +    virtual void group_started(const std::string& name) +    { +        mPrev->group_started(name); +    } + +    /** +     * Called when a test finished. +     * @param tr Test results. +     */ +    virtual void test_completed(const tut::test_result& tr) +    { +        mPrev->test_completed(tr); +    } + +    /** +     * Called when a group is completed +     * @param name Name of the group +     */ +    virtual void group_completed(const std::string& name) +    { +        mPrev->group_completed(name); +    } + +    /** +     * Called when all tests in run completed. +     */ +    virtual void run_completed() +    { +        mPrev->run_completed(); +    } + +private: +    tut::callback* mPrev; +}; + +#endif /* ! defined(LL_CHAINED_CALLBACK_H) */ diff --git a/indra/test/debug.h b/indra/test/debug.h index d61eba651b..76dbb973b2 100644 --- a/indra/test/debug.h +++ b/indra/test/debug.h @@ -29,42 +29,64 @@  #if ! defined(LL_DEBUG_H)  #define LL_DEBUG_H -#include <iostream> +#include "print.h"  /*****************************************************************************  *   Debugging stuff  *****************************************************************************/ -// This class is intended to illuminate entry to a given block, exit from the -// same block and checkpoints along the way. It also provides a convenient -// place to turn std::cout output on and off. +/** + * This class is intended to illuminate entry to a given block, exit from the + * same block and checkpoints along the way. It also provides a convenient + * place to turn std::cerr output on and off. + * + * If the environment variable LOGTEST is non-empty, each Debug instance will + * announce its construction and destruction, presumably at entry and exit to + * the block in which it's declared. Moreover, any arguments passed to its + * operator()() will be streamed to std::cerr, prefixed by the block + * description. + * + * The variable LOGTEST is used because that's the environment variable + * checked by test.cpp, our TUT main() program, to turn on LLError logging. It + * is expected that Debug is solely for use in test programs. + */  class Debug  {  public:      Debug(const std::string& block): -        mBlock(block) +        mBlock(block), +        mLOGTEST(getenv("LOGTEST")), +        // debug output enabled when LOGTEST is set AND non-empty +        mEnabled(mLOGTEST && *mLOGTEST)      {          (*this)("entry");      } +    // non-copyable +    Debug(const Debug&) = delete; +      ~Debug()      {          (*this)("exit");      } -    void operator()(const std::string& status) +    template <typename... ARGS> +    void operator()(ARGS&&... args)      { -#if defined(DEBUG_ON) -        std::cout << mBlock << ' ' << status << std::endl; -#endif +        if (mEnabled) +        { +            print(mBlock, ' ', std::forward<ARGS>(args)...); +        }      }  private:      const std::string mBlock; +    const char* mLOGTEST; +    bool mEnabled;  };  // It's often convenient to use the name of the enclosing function as the name  // of the Debug block. -#define DEBUG Debug debug(__FUNCTION__) +#define DEBUG Debug debug(LL_PRETTY_FUNCTION)  // These BEGIN/END macros are specifically for debugging output -- please  // don't assume you must use such for coroutines in general! They only help to diff --git a/indra/test/llevents_tut.cpp b/indra/test/llevents_tut.cpp index 3abae3e43e..17f64a4953 100644 --- a/indra/test/llevents_tut.cpp +++ b/indra/test/llevents_tut.cpp @@ -38,7 +38,6 @@  #define testable public  #include "llevents.h"  #undef testable -#include "lllistenerwrapper.h"  // STL headers  // std headers  #include <iostream> @@ -92,9 +91,7 @@ template<> template<>  void events_object::test<1>()  {  	set_test_name("basic operations"); -	// Now there's a static constructor in llevents.cpp that registers on -	// the "mainloop" pump to call LLEventPumps::flush(). -	// Actually -- having to modify this to track the statically- +	// Having to modify this to track the statically-  	// constructed pumps in other TUT modules in this giant monolithic test  	// executable isn't such a hot idea.  	// ensure_equals("initial pump", pumps.mPumpMap.size(), 1); @@ -211,43 +208,6 @@ bool chainEvents(Listener& someListener, const LLSD& event)  template<> template<>  void events_object::test<3>()  { -	set_test_name("LLEventQueue delayed action"); -	// This access is NOT legal usage: we can do it only because we're -	// hacking private for test purposes. Normally we'd either compile in -	// a particular name, or (later) edit a config file. -	pumps.mQueueNames.insert("login"); -	LLEventPump& login(pumps.obtain("login")); -	// The "mainloop" pump is special: posting on that implicitly calls -	// LLEventPumps::flush(), which in turn should flush our "login" -	// LLEventQueue. -	LLEventPump& mainloop(pumps.obtain("mainloop")); -	ensure("LLEventQueue leaf class", dynamic_cast<LLEventQueue*> (&login)); -	listener0.listenTo(login); -	listener0.reset(0); -	login.post(1); -	check_listener("waiting for queued event", listener0, 0); -	mainloop.post(LLSD()); -	check_listener("got queued event", listener0, 1); -	login.stopListening(listener0.getName()); -	// Verify that when an event handler posts a new event on the same -	// LLEventQueue, it doesn't get processed in the same flush() call -- -	// it waits until the next flush() call. -	listener0.reset(17); -	login.listen("chainEvents", boost::bind(chainEvents, boost::ref(listener0), _1)); -	login.post(1); -	check_listener("chainEvents(1) not yet called", listener0, 17); -	mainloop.post(LLSD()); -	check_listener("chainEvents(1) called", listener0, 1); -	mainloop.post(LLSD()); -	check_listener("chainEvents(0) called", listener0, 0); -	mainloop.post(LLSD()); -	check_listener("chainEvents(-1) not called", listener0, 0); -	login.stopListening("chainEvents"); -} - -template<> template<> -void events_object::test<4>() -{  	set_test_name("explicitly-instantiated LLEventStream");  	// Explicitly instantiate an LLEventStream, and verify that it  	// self-registers with LLEventPumps @@ -271,7 +231,7 @@ void events_object::test<4>()  }  template<> template<> -void events_object::test<5>() +void events_object::test<4>()  {  	set_test_name("stopListening()");  	LLEventPump& login(pumps.obtain("login")); @@ -285,7 +245,7 @@ void events_object::test<5>()  }  template<> template<> -void events_object::test<6>() +void events_object::test<5>()  {  	set_test_name("chaining LLEventPump instances");  	LLEventPump& upstream(pumps.obtain("upstream")); @@ -310,7 +270,7 @@ void events_object::test<6>()  }  template<> template<> -void events_object::test<7>() +void events_object::test<6>()  {  	set_test_name("listener dependency order");  	typedef LLEventPump::NameList NameList; @@ -392,7 +352,7 @@ void events_object::test<7>()  }  template<> template<> -void events_object::test<8>() +void events_object::test<7>()  {  	set_test_name("tweaked and untweaked LLEventPump instance names");  	{ 	// nested scope @@ -424,7 +384,7 @@ void eventSource(const LLListenerOrPumpName& listener)  }  template<> template<> -void events_object::test<9>() +void events_object::test<8>()  {  	set_test_name("LLListenerOrPumpName");  	// Passing a boost::bind() expression to LLListenerOrPumpName @@ -465,7 +425,7 @@ private:  };  template<> template<> -void events_object::test<10>() +void events_object::test<9>()  {  	set_test_name("listen(boost::bind(...TempListener...))");  	// listen() can't do anything about a plain TempListener instance: @@ -493,223 +453,60 @@ void events_object::test<10>()  	heaptest.stopListening("temp");  } -template<> template<> -void events_object::test<11>() -{ -	set_test_name("listen(boost::bind(...weak_ptr...))"); -	// listen() detecting weak_ptr<TempListener> in boost::bind() object -	bool live = false; -	LLEventPump& heaptest(pumps.obtain("heaptest")); -	LLBoundListener connection; -	ensure("default state", !connection.connected()); -	{ -		boost::shared_ptr<TempListener> newListener(new TempListener("heap", live)); -		newListener->reset(); -		ensure("TempListener constructed", live); -		connection = heaptest.listen(newListener->getName(), -									 boost::bind(&Listener::call,  -												 weaken(newListener),  -												 _1)); -		ensure("new connection", connection.connected()); -		heaptest.post(1); -		check_listener("received", *newListener, 1); -	} // presumably this will make newListener go away? -	// verify that -	ensure("TempListener destroyed", !live); -	ensure("implicit disconnect", !connection.connected()); -	// now just make sure we don't blow up trying to access a freed object! -	heaptest.post(2); -} - -template<> template<> -void events_object::test<12>() -{ -	set_test_name("listen(boost::bind(...shared_ptr...))"); -	/*==========================================================================*| -	// DISABLED because I've made this case produce a compile error. -	// Following the error leads the disappointed dev to a comment -	// instructing her to use the weaken() function to bind a weak_ptr<T> -	// instead of binding a shared_ptr<T>, and explaining why. I know of -	// no way to use TUT to code a repeatable test in which the expected -	// outcome is a compile error. The interested reader is invited to -	// uncomment this block and build to see for herself. - -	// listen() detecting shared_ptr<TempListener> in boost::bind() object -	bool live = false; -	LLEventPump& heaptest(pumps.obtain("heaptest")); -	LLBoundListener connection; -	std::string listenerName("heap"); -	ensure("default state", !connection.connected()); -	{ -		boost::shared_ptr<TempListener> newListener(new TempListener(listenerName, live)); -		ensure_equals("use_count", newListener.use_count(), 1); -		newListener->reset(); -		ensure("TempListener constructed", live); -		connection = heaptest.listen(newListener->getName(), -									 boost::bind(&Listener::call, newListener, _1)); -		ensure("new connection", connection.connected()); -		ensure_equals("use_count", newListener.use_count(), 2); -		heaptest.post(1); -		check_listener("received", *newListener, 1); -	} // this should make newListener go away... -	// Unfortunately, the fact that we've bound a shared_ptr by value into -	// our LLEventPump means that copy will keep the referenced object alive. -	ensure("TempListener still alive", live); -	ensure("still connected", connection.connected()); -	// disconnecting explicitly should delete the TempListener... -	heaptest.stopListening(listenerName); -#if 0   // however, in my experience, it does not. I don't know why not. -	// Ah: on 2009-02-19, Frank Mori Hess, author of the Boost.Signals2 -	// library, stated on the boost-users mailing list: -	// http://www.nabble.com/Re%3A--signals2--review--The-review-of-the-signals2-library-(formerly-thread_safe_signals)-begins-today%2C-Nov-1st-p22102367.html -	// "It will get destroyed eventually. The signal cleans up its slot -	// list little by little during connect/invoke. It doesn't immediately -	// remove disconnected slots from the slot list since other threads -	// might be using the same slot list concurrently. It might be -	// possible to make it immediately reset the shared_ptr owning the -	// slot though, leaving an empty shared_ptr in the slot list, since -	// that wouldn't invalidate any iterators." -	ensure("TempListener destroyed", ! live); -	ensure("implicit disconnect", ! connection.connected()); -#endif  // 0 -	// now just make sure we don't blow up trying to access a freed object! -	heaptest.post(2); -|*==========================================================================*/ -} -  class TempTrackableListener: public TempListener, public LLEventTrackable  {  public: -TempTrackableListener(const std::string& name, bool& liveFlag): -	TempListener(name, liveFlag) -{} +    TempTrackableListener(const std::string& name, bool& liveFlag): +        TempListener(name, liveFlag) +    {}  };  template<> template<> -void events_object::test<13>() -{ -set_test_name("listen(boost::bind(...TempTrackableListener ref...))"); -bool live = false; -LLEventPump& heaptest(pumps.obtain("heaptest")); -LLBoundListener connection; -{ -	TempTrackableListener tempListener("temp", live); -	ensure("TempTrackableListener constructed", live); -	connection = heaptest.listen(tempListener.getName(), -								 boost::bind(&TempTrackableListener::call, -											 boost::ref(tempListener), _1)); -	heaptest.post(1); -	check_listener("received", tempListener, 1); -} // presumably this will make tempListener go away? -// verify that -ensure("TempTrackableListener destroyed", ! live); -ensure("implicit disconnect", ! connection.connected()); -// now just make sure we don't blow up trying to access a freed object! -heaptest.post(2); -} - -template<> template<> -void events_object::test<14>() -{ -set_test_name("listen(boost::bind(...TempTrackableListener pointer...))"); -bool live = false; -LLEventPump& heaptest(pumps.obtain("heaptest")); -LLBoundListener connection; +void events_object::test<10>()  { -	TempTrackableListener* newListener(new TempTrackableListener("temp", live)); -	ensure("TempTrackableListener constructed", live); -	connection = heaptest.listen(newListener->getName(), -								 boost::bind(&TempTrackableListener::call, -											 newListener, _1)); -	heaptest.post(1); -	check_listener("received", *newListener, 1); -	// explicitly destroy newListener -	delete newListener; -} -// verify that -ensure("TempTrackableListener destroyed", ! live); -ensure("implicit disconnect", ! connection.connected()); -// now just make sure we don't blow up trying to access a freed object! -heaptest.post(2); +    set_test_name("listen(boost::bind(...TempTrackableListener ref...))"); +    bool live = false; +    LLEventPump& heaptest(pumps.obtain("heaptest")); +    LLBoundListener connection; +    { +        TempTrackableListener tempListener("temp", live); +        ensure("TempTrackableListener constructed", live); +        connection = heaptest.listen(tempListener.getName(), +                                     boost::bind(&TempTrackableListener::call, +                                                 boost::ref(tempListener), _1)); +        heaptest.post(1); +        check_listener("received", tempListener, 1); +    } // presumably this will make tempListener go away? +    // verify that +    ensure("TempTrackableListener destroyed", ! live); +    ensure("implicit disconnect", ! connection.connected()); +    // now just make sure we don't blow up trying to access a freed object! +    heaptest.post(2);  }  template<> template<> -void events_object::test<15>() -{ -// This test ensures that using an LLListenerWrapper subclass doesn't -// block Boost.Signals2 from recognizing a bound LLEventTrackable -// subclass. -set_test_name("listen(llwrap<LLLogListener>(boost::bind(...TempTrackableListener ref...)))"); -bool live = false; -LLEventPump& heaptest(pumps.obtain("heaptest")); -LLBoundListener connection; +void events_object::test<11>()  { -	TempTrackableListener tempListener("temp", live); -	ensure("TempTrackableListener constructed", live); -	connection = heaptest.listen(tempListener.getName(), -								 llwrap<LLLogListener>( -								 boost::bind(&TempTrackableListener::call, -											 boost::ref(tempListener), _1))); -	heaptest.post(1); -	check_listener("received", tempListener, 1); -} // presumably this will make tempListener go away? -// verify that -ensure("TempTrackableListener destroyed", ! live); -ensure("implicit disconnect", ! connection.connected()); -// now just make sure we don't blow up trying to access a freed object! -heaptest.post(2); +    set_test_name("listen(boost::bind(...TempTrackableListener pointer...))"); +    bool live = false; +    LLEventPump& heaptest(pumps.obtain("heaptest")); +    LLBoundListener connection; +    { +        TempTrackableListener* newListener(new TempTrackableListener("temp", live)); +        ensure("TempTrackableListener constructed", live); +        connection = heaptest.listen(newListener->getName(), +                                     boost::bind(&TempTrackableListener::call, +                                                 newListener, _1)); +        heaptest.post(1); +        check_listener("received", *newListener, 1); +        // explicitly destroy newListener +        delete newListener; +    } +    // verify that +    ensure("TempTrackableListener destroyed", ! live); +    ensure("implicit disconnect", ! connection.connected()); +    // now just make sure we don't blow up trying to access a freed object! +    heaptest.post(2);  } -class TempSharedListener: public TempListener, -public boost::enable_shared_from_this<TempSharedListener> -{ -public: -TempSharedListener(const std::string& name, bool& liveFlag): -	TempListener(name, liveFlag) -{} -}; - -template<> template<> -void events_object::test<16>() -{ -	set_test_name("listen(boost::bind(...TempSharedListener ref...))"); -#if 0 -bool live = false; -LLEventPump& heaptest(pumps.obtain("heaptest")); -LLBoundListener connection; -{ -	// We MUST have at least one shared_ptr to an -	// enable_shared_from_this subclass object before -	// shared_from_this() can work. -	boost::shared_ptr<TempSharedListener> -		tempListener(new TempSharedListener("temp", live)); -	ensure("TempSharedListener constructed", live); -	// However, we're not passing either the shared_ptr or its -	// corresponding weak_ptr -- instead, we're passing a reference to -	// the TempSharedListener. -/*==========================================================================*| -	 std::cout << "Capturing const ref" << std::endl; -	 const boost::enable_shared_from_this<TempSharedListener>& cref(*tempListener); -	 std::cout << "Capturing const ptr" << std::endl; -	 const boost::enable_shared_from_this<TempSharedListener>* cp(&cref); -	 std::cout << "Capturing non-const ptr" << std::endl; -	 boost::enable_shared_from_this<TempSharedListener>* p(const_cast<boost::enable_shared_from_this<TempSharedListener>*>(cp)); -	 std::cout << "Capturing shared_from_this()" << std::endl; -	 boost::shared_ptr<TempSharedListener> sp(p->shared_from_this()); -	 std::cout << "Capturing weak_ptr" << std::endl; -	 boost::weak_ptr<TempSharedListener> wp(weaken(sp)); -	 std::cout << "Binding weak_ptr" << std::endl; -|*==========================================================================*/ -	connection = heaptest.listen(tempListener->getName(), -								 boost::bind(&TempSharedListener::call, *tempListener, _1)); -	heaptest.post(1); -	check_listener("received", *tempListener, 1); -} // presumably this will make tempListener go away? -// verify that -ensure("TempSharedListener destroyed", ! live); -ensure("implicit disconnect", ! connection.connected()); -// now just make sure we don't blow up trying to access a freed object! -heaptest.post(2); -#endif // 0 -}  } // namespace tut diff --git a/indra/test/lltestapp.h b/indra/test/lltestapp.h new file mode 100644 index 0000000000..382516cd2b --- /dev/null +++ b/indra/test/lltestapp.h @@ -0,0 +1,34 @@ +/** + * @file   lltestapp.h + * @author Nat Goodspeed + * @date   2019-10-21 + * @brief  LLApp subclass useful for testing. + *  + * $LicenseInfo:firstyear=2019&license=viewerlgpl$ + * Copyright (c) 2019, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_LLTESTAPP_H) +#define LL_LLTESTAPP_H + +#include "llapp.h" + +/** + * LLTestApp is a dummy LLApp that simply sets LLApp::isRunning() for anyone + * who cares. + */ +class LLTestApp: public LLApp +{ +public: +    LLTestApp() +    { +        setStatus(APP_STATUS_RUNNING); +    } + +    bool init()    { return true; } +    bool cleanup() { return true; } +    bool frame()   { return true; } +}; + +#endif /* ! defined(LL_LLTESTAPP_H) */ diff --git a/indra/test/print.h b/indra/test/print.h new file mode 100644 index 0000000000..08e36caddf --- /dev/null +++ b/indra/test/print.h @@ -0,0 +1,42 @@ +/** + * @file   print.h + * @author Nat Goodspeed + * @date   2020-01-02 + * @brief  print() function for debugging + *  + * $LicenseInfo:firstyear=2020&license=viewerlgpl$ + * Copyright (c) 2020, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_PRINT_H) +#define LL_PRINT_H + +#include <iostream> + +// print(..., NONL); +// leaves the output dangling, suppressing the normally appended std::endl +struct NONL_t {}; +#define NONL (NONL_t()) + +// normal recursion end +inline +void print() +{ +    std::cerr << std::endl; +} + +// print(NONL) is a no-op +inline +void print(NONL_t) +{ +} + +template <typename T, typename... ARGS> +void print(T&& first, ARGS&&... rest) +{ +    std::cerr << first; +    print(std::forward<ARGS>(rest)...); +} + +#endif /* ! defined(LL_PRINT_H) */ diff --git a/indra/test/setenv.h b/indra/test/setenv.h new file mode 100644 index 0000000000..ed2de9ccca --- /dev/null +++ b/indra/test/setenv.h @@ -0,0 +1,66 @@ +/** + * @file   setenv.h + * @author Nat Goodspeed + * @date   2020-04-01 + * @brief  Provide a way for a particular test program to alter the + *         environment before entry to main(). + *  + * $LicenseInfo:firstyear=2020&license=viewerlgpl$ + * Copyright (c) 2020, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_SETENV_H) +#define LL_SETENV_H + +#include <stdlib.h>                 // setenv() + +/** + * Our test.cpp main program responds to environment variables LOGTEST and + * LOGFAIL. But if you set (e.g.) LOGTEST=DEBUG before a viewer build, @em + * every test program in the build emits debug log output. This can be so + * voluminous as to slow down the build. + * + * With an integration test program, you can specifically build (e.g.) the + * INTEGRATION_TEST_llstring target, and set any environment variables you + * want for that. But with a unit test program, since executing the program is + * a side effect rather than an explicit target, specifically building (e.g.) + * PROJECT_lllogin_TEST_lllogin only builds the executable without running it. + * + * To set an environment variable for a particular test program, declare a + * static instance of SetEnv in its .cpp file. SetEnv's constructor takes + * pairs of strings, e.g. + * + * @code + * static SetEnv sLOGGING("LOGTEST", "INFO"); + * @endcode + * + * Declaring a static instance of SetEnv is important because that ensures + * that the environment variables are set before main() is entered, since it + * is main() that examines LOGTEST and LOGFAIL. + */ +struct SetEnv +{ +    // degenerate constructor, terminate recursion +    SetEnv() {} + +    /** +     * SetEnv() accepts an arbitrary number of pairs of strings: variable +     * name, value, variable name, value ... Entering the constructor sets +     * those variables in the process environment using Posix setenv(), +     * overriding any previous value. If static SetEnv declarations in +     * different translation units specify overlapping sets of variable names, +     * it is indeterminate which instance will "win." +     */ +    template <typename VAR, typename VAL, typename... ARGS> +    SetEnv(VAR&& var, VAL&& val, ARGS&&... rest): +        // constructor forwarding handles the tail of the list +        SetEnv(std::forward<ARGS>(rest)...) +    { +        // set just the first (variable, value) pair +        // 1 means override previous value if any +        setenv(std::forward<VAR>(var), std::forward<VAL>(val), 1); +    } +}; + +#endif /* ! defined(LL_SETENV_H) */ diff --git a/indra/test/sync.h b/indra/test/sync.h new file mode 100644 index 0000000000..ca8b7262d6 --- /dev/null +++ b/indra/test/sync.h @@ -0,0 +1,116 @@ +/** + * @file   sync.h + * @author Nat Goodspeed + * @date   2019-03-13 + * @brief  Synchronize coroutines within a test program so we can observe side + *         effects. Certain test programs test coroutine synchronization + *         mechanisms. Such tests usually want to interleave coroutine + *         executions in strictly stepwise fashion. This class supports that + *         paradigm. + *  + * $LicenseInfo:firstyear=2019&license=viewerlgpl$ + * Copyright (c) 2019, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_SYNC_H) +#define LL_SYNC_H + +#include "llcond.h" +#include "lltut.h" +#include "stringize.h" +#include "llerror.h" +#include "llcoros.h" + +/** + * Instantiate Sync in any test in which we need to suspend one coroutine + * until we're sure that another has had a chance to run. Simply calling + * llcoro::suspend() isn't necessarily enough; that provides a chance for the + * other to run, but doesn't guarantee that it has. If each coroutine is + * consistent about calling Sync::bump() every time it wakes from any + * suspension, Sync::yield() and yield_until() should at least ensure that + * somebody else has had a chance to run. + */ +class Sync +{ +    LLScalarCond<int> mCond{0}; +    F32Milliseconds mTimeout; + +public: +    Sync(F32Milliseconds timeout=F32Milliseconds(10000.0f)): +        mTimeout(timeout) +    {} + +    /** +     * Bump mCond by n steps -- ideally, do this every time a participating +     * coroutine wakes up from any suspension. The choice to bump() after +     * resumption rather than just before suspending is worth calling out: +     * this practice relies on the fact that condition_variable::notify_all() +     * merely marks a suspended coroutine ready to run, rather than +     * immediately resuming it. This way, though, even if a coroutine exits +     * before reaching its next suspend point, the other coroutine isn't +     * left waiting forever. +     */ +    void bump(int n=1) +    { +        // Calling mCond.set_all(mCond.get() + n) would be great for +        // coroutines -- but not so good between kernel threads -- it would be +        // racy. Make the increment atomic by calling update_all(), which runs +        // the passed lambda within a mutex lock. +        int updated; +        mCond.update_all( +            [&n, &updated](int& data) +            { +                data += n; +                // Capture the new value for possible logging purposes. +                updated = data; +            }); +        // In the multi-threaded case, this log message could be a bit +        // misleading, as it will be emitted after waiting threads have +        // already awakened. But emitting the log message within the lock +        // would seem to hold the lock longer than we really ought. +        LL_DEBUGS() << llcoro::logname() << " bump(" << n << ") -> " << updated << LL_ENDL; +    } + +    /** +     * Set mCond to a specific n. Use of bump() and yield() is nicely +     * maintainable, since you can insert or delete matching operations in a +     * test function and have the rest of the Sync operations continue to +     * line up as before. But sometimes you need to get very specific, which +     * is where set() and yield_until() come in handy: less maintainable, +     * more precise. +     */ +    void set(int n) +    { +        LL_DEBUGS() << llcoro::logname() << " set(" << n << ")" << LL_ENDL; +        mCond.set_all(n); +    } + +    /// suspend until "somebody else" has bumped mCond by n steps +    void yield(int n=1) +    { +        return yield_until(STRINGIZE("Sync::yield_for(" << n << ") timed out after " +                                     << int(mTimeout.value()) << "ms"), +                           mCond.get() + n); +    } + +    /// suspend until "somebody else" has bumped mCond to a specific value +    void yield_until(int until) +    { +        return yield_until(STRINGIZE("Sync::yield_until(" << until << ") timed out after " +                                     << int(mTimeout.value()) << "ms"), +                           until); +    } + +private: +    void yield_until(const std::string& desc, int until) +    { +        std::string name(llcoro::logname()); +        LL_DEBUGS() << name << " yield_until(" << until << ") suspending" << LL_ENDL; +        tut::ensure(name + ' ' + desc, mCond.wait_for_equal(mTimeout, until)); +        // each time we wake up, bump mCond +        bump(); +    } +}; + +#endif /* ! defined(LL_SYNC_H) */ diff --git a/indra/test/test.cpp b/indra/test/test.cpp index b14c2eb255..87c4a8d8a3 100644 --- a/indra/test/test.cpp +++ b/indra/test/test.cpp @@ -37,6 +37,7 @@  #include "linden_common.h"  #include "llerrorcontrol.h"  #include "lltut.h" +#include "chained_callback.h"  #include "stringize.h"  #include "namedtempfile.h"  #include "lltrace.h" @@ -71,7 +72,6 @@  #include <boost/shared_ptr.hpp>  #include <boost/make_shared.hpp>  #include <boost/foreach.hpp> -#include <boost/lambda/lambda.hpp>  #include <fstream> @@ -172,8 +172,10 @@ private:  	LLError::RecorderPtr mRecorder;  }; -class LLTestCallback : public tut::callback +class LLTestCallback : public chained_callback  { +	typedef chained_callback super; +  public:  	LLTestCallback(bool verbose_mode, std::ostream *stream,  				   boost::shared_ptr<LLReplayLog> replayer) : @@ -184,7 +186,7 @@ public:  		mSkippedTests(0),  		// By default, capture a shared_ptr to std::cout, with a no-op "deleter"  		// so that destroying the shared_ptr makes no attempt to delete std::cout. -		mStream(boost::shared_ptr<std::ostream>(&std::cout, boost::lambda::_1)), +		mStream(boost::shared_ptr<std::ostream>(&std::cout, [](std::ostream*){})),  		mReplayer(replayer)  	{  		if (stream) @@ -205,22 +207,25 @@ public:  	~LLTestCallback()  	{ -	}	 +	}  	virtual void run_started()  	{  		//std::cout << "run_started" << std::endl;  		LL_INFOS("TestRunner")<<"Test Started"<< LL_ENDL; +		super::run_started();  	}  	virtual void group_started(const std::string& name) {  		LL_INFOS("TestRunner")<<"Unit test group_started name=" << name << LL_ENDL;  		*mStream << "Unit test group_started name=" << name << std::endl; +		super::group_started(name);  	}  	virtual void group_completed(const std::string& name) {  		LL_INFOS("TestRunner")<<"Unit test group_completed name=" << name << LL_ENDL;  		*mStream << "Unit test group_completed name=" << name << std::endl; +		super::group_completed(name);  	}  	virtual void test_completed(const tut::test_result& tr) @@ -282,6 +287,7 @@ public:  			*mStream << std::endl;  		}  		LL_INFOS("TestRunner")<<out.str()<<LL_ENDL; +		super::test_completed(tr);  	}  	virtual int getFailedTests() const { return mFailedTests; } @@ -309,6 +315,7 @@ public:  			*mStream << "Please report or fix the problem." << std::endl;  			*mStream << "*********************************" << std::endl;  		} +		super::run_completed();  	}  protected: @@ -474,9 +481,8 @@ void stream_usage(std::ostream& s, const char* app)  	  << "LOGTEST=level : for all tests, emit log messages at level 'level'\n"  	  << "LOGFAIL=level : only for failed tests, emit log messages at level 'level'\n"  	  << "where 'level' is one of ALL, DEBUG, INFO, WARN, ERROR, NONE.\n" -	  << "--debug is like LOGTEST=DEBUG, but --debug overrides LOGTEST.\n" -	  << "Setting LOGFAIL overrides both LOGTEST and --debug: the only log\n" -	  << "messages you will see will be for failed tests.\n\n"; +	  << "--debug is like LOGTEST=DEBUG, but --debug overrides LOGTEST,\n" +	  << "while LOGTEST overrides LOGFAIL.\n\n";  	s << "Examples:" << std::endl;  	s << "  " << app << " --verbose" << std::endl; @@ -520,35 +526,8 @@ int main(int argc, char **argv)  #ifndef LL_WINDOWS  	::testing::InitGoogleMock(&argc, argv);  #endif -	// LOGTEST overrides default, but can be overridden by --debug or LOGFAIL. -	const char* LOGTEST = getenv("LOGTEST"); -	if (LOGTEST) -	{ -		LLError::initForApplication(".", ".", true /* log to stderr */); -		LLError::setDefaultLevel(LLError::decodeLevel(LOGTEST)); -	} -	else -	{ -		LLError::initForApplication(".", ".", false /* do not log to stderr */); -		LLError::setDefaultLevel(LLError::LEVEL_DEBUG); -	}	 -	LLError::setFatalFunction(wouldHaveCrashed); -	std::string test_app_name(argv[0]); -	std::string test_log = test_app_name + ".log"; -	LLFile::remove(test_log); -	LLError::logToFile(test_log); - -#ifdef CTYPE_WORKAROUND -	ctype_workaround(); -#endif  	ll_init_apr(); -	 -	if (!sMasterThreadRecorder) -	{ -		sMasterThreadRecorder = new LLTrace::ThreadRecorder(); -		LLTrace::set_master_thread_recorder(sMasterThreadRecorder); -	}  	apr_getopt_t* os = NULL;  	if(APR_SUCCESS != apr_getopt_init(&os, gAPRPoolp, argc, argv))  	{ @@ -562,7 +541,10 @@ int main(int argc, char **argv)  	std::string test_group;  	std::string suite_name; -	// values use for options parsing +	// LOGTEST overrides default, but can be overridden by --debug. +	const char* LOGTEST = getenv("LOGTEST"); + +	// values used for options parsing  	apr_status_t apr_err;  	const char* opt_arg = NULL;  	int opt_id = 0; @@ -611,7 +593,7 @@ int main(int argc, char **argv)  				wait_at_exit = true;  				break;  			case 'd': -				LLError::setDefaultLevel(LLError::LEVEL_DEBUG); +				LOGTEST = "DEBUG";  				break;  			case 'x':  				suite_name.assign(opt_arg); @@ -623,22 +605,45 @@ int main(int argc, char **argv)  		}  	} -	// run the tests - +	// set up logging  	const char* LOGFAIL = getenv("LOGFAIL"); -	boost::shared_ptr<LLReplayLog> replayer; -	// As described in stream_usage(), LOGFAIL overrides both --debug and -	// LOGTEST. -	if (LOGFAIL) +	boost::shared_ptr<LLReplayLog> replayer{boost::make_shared<LLReplayLog>()}; + +	// Testing environment variables for both 'set' and 'not empty' allows a +	// user to suppress a pre-existing environment variable by forcing empty. +	if (LOGTEST && *LOGTEST)  	{ -		LLError::ELevel level = LLError::decodeLevel(LOGFAIL); -		replayer.reset(new LLReplayLogReal(level, gAPRPoolp)); +		LLError::initForApplication(".", ".", true /* log to stderr */); +		LLError::setDefaultLevel(LLError::decodeLevel(LOGTEST));  	}  	else  	{ -		replayer.reset(new LLReplayLog()); +		LLError::initForApplication(".", ".", false /* do not log to stderr */); +		LLError::setDefaultLevel(LLError::LEVEL_DEBUG); +		if (LOGFAIL && *LOGFAIL) +		{ +			LLError::ELevel level = LLError::decodeLevel(LOGFAIL); +			replayer.reset(new LLReplayLogReal(level, gAPRPoolp)); +		} +	} +	LLError::setFatalFunction(wouldHaveCrashed); +	std::string test_app_name(argv[0]); +	std::string test_log = test_app_name + ".log"; +	LLFile::remove(test_log); +	LLError::logToFile(test_log); + +#ifdef CTYPE_WORKAROUND +	ctype_workaround(); +#endif + +	if (!sMasterThreadRecorder) +	{ +		sMasterThreadRecorder = new LLTrace::ThreadRecorder(); +		LLTrace::set_master_thread_recorder(sMasterThreadRecorder);  	} +	// run the tests +  	LLTestCallback* mycallback;  	if (getenv("TEAMCITY_PROJECT_NAME"))  	{ @@ -649,7 +654,8 @@ int main(int argc, char **argv)  		mycallback = new LLTestCallback(verbose_mode, output.get(), replayer);  	} -	tut::runner.get().set_callback(mycallback); +	// a chained_callback subclass must be linked with previous +	mycallback->link();  	if(test_group.empty())  	{ | 
