diff options
Diffstat (limited to 'indra/llcommon/tests')
| -rw-r--r-- | indra/llcommon/tests/StringVec.h | 37 | ||||
| -rw-r--r-- | indra/llcommon/tests/listener.h | 21 | ||||
| -rw-r--r-- | indra/llcommon/tests/llerror_test.cpp | 226 | ||||
| -rw-r--r-- | indra/llcommon/tests/llinstancetracker_test.cpp | 69 | ||||
| -rw-r--r-- | indra/llcommon/tests/llleap_test.cpp | 694 | ||||
| -rw-r--r-- | indra/llcommon/tests/llprocess_test.cpp | 1262 | ||||
| -rw-r--r-- | indra/llcommon/tests/llsdserialize_test.cpp | 274 | ||||
| -rw-r--r-- | indra/llcommon/tests/llstreamqueue_test.cpp | 197 | ||||
| -rw-r--r-- | indra/llcommon/tests/llstring_test.cpp | 118 | ||||
| -rw-r--r-- | indra/llcommon/tests/setpython.py | 19 | ||||
| -rw-r--r-- | indra/llcommon/tests/wrapllerrs.h | 129 | 
11 files changed, 2639 insertions, 407 deletions
| diff --git a/indra/llcommon/tests/StringVec.h b/indra/llcommon/tests/StringVec.h new file mode 100644 index 0000000000..a380b00a05 --- /dev/null +++ b/indra/llcommon/tests/StringVec.h @@ -0,0 +1,37 @@ +/** + * @file   StringVec.h + * @author Nat Goodspeed + * @date   2012-02-24 + * @brief  Extend TUT ensure_equals() to handle std::vector<std::string> + *  + * $LicenseInfo:firstyear=2012&license=viewerlgpl$ + * Copyright (c) 2012, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_STRINGVEC_H) +#define LL_STRINGVEC_H + +#include <vector> +#include <string> +#include <iostream> + +typedef std::vector<std::string> StringVec; + +std::ostream& operator<<(std::ostream& out, const StringVec& strings) +{ +    out << '('; +    StringVec::const_iterator begin(strings.begin()), end(strings.end()); +    if (begin != end) +    { +        out << '"' << *begin << '"'; +        while (++begin != end) +        { +            out << ", \"" << *begin << '"'; +        } +    } +    out << ')'; +    return out; +} + +#endif /* ! defined(LL_STRINGVEC_H) */ diff --git a/indra/llcommon/tests/listener.h b/indra/llcommon/tests/listener.h index dcdb2412be..9c5c18a150 100644 --- a/indra/llcommon/tests/listener.h +++ b/indra/llcommon/tests/listener.h @@ -30,6 +30,8 @@  #define LL_LISTENER_H  #include "llsd.h" +#include "llevents.h" +#include "tests/StringVec.h"  #include <iostream>  /***************************************************************************** @@ -133,24 +135,7 @@ struct Collect          return false;      }      void clear() { result.clear(); } -    typedef std::vector<std::string> StringList; -    StringList result; +    StringVec result;  }; -std::ostream& operator<<(std::ostream& out, const Collect::StringList& strings) -{ -    out << '('; -    Collect::StringList::const_iterator begin(strings.begin()), end(strings.end()); -    if (begin != end) -    { -        out << '"' << *begin << '"'; -        while (++begin != end) -        { -            out << ", \"" << *begin << '"'; -        } -    } -    out << ')'; -    return out; -} -  #endif /* ! defined(LL_LISTENER_H) */ diff --git a/indra/llcommon/tests/llerror_test.cpp b/indra/llcommon/tests/llerror_test.cpp index 09a20231de..279a90e51b 100644 --- a/indra/llcommon/tests/llerror_test.cpp +++ b/indra/llcommon/tests/llerror_test.cpp @@ -1,4 +1,4 @@ -/**  +/**   * @file llerror_test.cpp   * @date   December 2006   * @brief error unit tests @@ -6,21 +6,21 @@   * $LicenseInfo:firstyear=2006&license=viewerlgpl$   * Second Life Viewer Source Code   * Copyright (C) 2010, Linden Research, Inc. - *  + *   * This library is free software; you can redistribute it and/or   * modify it under the terms of the GNU Lesser General Public   * License as published by the Free Software Foundation;   * version 2.1 of the License only. - *  + *   * This library is distributed in the hope that it will be useful,   * but WITHOUT ANY WARRANTY; without even the implied warranty of   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU   * Lesser General Public License for more details. - *  + *   * You should have received a copy of the GNU Lesser General Public   * License along with this library; if not, write to the Free Software   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA - *  + *   * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA   * $/LicenseInfo$   */ @@ -49,7 +49,7 @@ namespace  	static bool fatalWasCalled;  	void fatalCall(const std::string&) { fatalWasCalled = true; }  } -	 +  namespace tut  {  	class TestRecorder : public LLError::Recorder @@ -57,59 +57,65 @@ namespace tut  	public:  		TestRecorder() : mWantsTime(false) { }  		~TestRecorder() { LLError::removeRecorder(this); } -		 +  		void recordMessage(LLError::ELevel level,  						   const std::string& message)  		{  			mMessages.push_back(message);  		} -		 +  		int countMessages()			{ return (int) mMessages.size(); }  		void clearMessages()		{ mMessages.clear(); } -		 +  		void setWantsTime(bool t)	{ mWantsTime = t; }  		bool wantsTime()			{ return mWantsTime; } -		 +  		std::string message(int n)  		{  			std::ostringstream test_name;  			test_name << "testing message " << n << ", not enough messages"; -			 +  			tut::ensure(test_name.str(), n < countMessages());  			return mMessages[n];  		} -		 +  	private:  		typedef std::vector<std::string> MessageVector;  		MessageVector mMessages; -		 +  		bool mWantsTime;  	};  	struct ErrorTestData  	{ -		TestRecorder mRecorder; +		// addRecorder() expects to be able to later delete the passed +		// Recorder*. Even though removeRecorder() reclaims ownership, passing +		// a pointer to a data member rather than a heap Recorder subclass +		// instance would just be Wrong. +		TestRecorder* mRecorder;  		LLError::Settings* mPriorErrorSettings; -		 -		ErrorTestData() + +		ErrorTestData(): +			mRecorder(new TestRecorder)  		{  			fatalWasCalled = false; -			 +  			mPriorErrorSettings = LLError::saveAndResetSettings();  			LLError::setDefaultLevel(LLError::LEVEL_DEBUG);  			LLError::setFatalFunction(fatalCall); -			LLError::addRecorder(&mRecorder); +			LLError::addRecorder(mRecorder);  		} -		 +  		~ErrorTestData()  		{ -			LLError::removeRecorder(&mRecorder); +			LLError::removeRecorder(mRecorder); +			delete mRecorder;  			LLError::restoreSettings(mPriorErrorSettings);  		} -		 +  		void ensure_message_count(int expectedCount)  		{ -			ensure_equals("message count", mRecorder.countMessages(), expectedCount); +			ensure_equals("message count", mRecorder->countMessages(), expectedCount);  		}  		void ensure_message_contains(int n, const std::string& expectedText) @@ -117,7 +123,7 @@ namespace tut  			std::ostringstream test_name;  			test_name << "testing message " << n; -			ensure_contains(test_name.str(), mRecorder.message(n), expectedText); +			ensure_contains(test_name.str(), mRecorder->message(n), expectedText);  		}  		void ensure_message_does_not_contain(int n, const std::string& expectedText) @@ -125,22 +131,22 @@ namespace tut  			std::ostringstream test_name;  			test_name << "testing message " << n; -			ensure_does_not_contain(test_name.str(), mRecorder.message(n), expectedText); +			ensure_does_not_contain(test_name.str(), mRecorder->message(n), expectedText);  		}  	}; -	 +  	typedef test_group<ErrorTestData>	ErrorTestGroup;  	typedef ErrorTestGroup::object		ErrorTestObject; -	 +  	ErrorTestGroup errorTestGroup("error"); -	 +  	template<> template<>  	void ErrorTestObject::test<1>()  		// basic test of output  	{  		llinfos << "test" << llendl;  		llinfos << "bob" << llendl; -		 +  		ensure_message_contains(0, "test");  		ensure_message_contains(1, "bob");  	} @@ -159,7 +165,7 @@ namespace  };  namespace tut -{	 +{  	template<> template<>  	void ErrorTestObject::test<2>()  		// messages are filtered based on default level @@ -172,7 +178,7 @@ namespace tut  		ensure_message_contains(3, "error");  		ensure_message_contains(4, "four");  		ensure_message_count(5); -	 +  		LLError::setDefaultLevel(LLError::LEVEL_INFO);  		writeSome();  		ensure_message_contains(5, "two"); @@ -180,20 +186,20 @@ namespace tut  		ensure_message_contains(7, "error");  		ensure_message_contains(8, "four");  		ensure_message_count(9); -		 +  		LLError::setDefaultLevel(LLError::LEVEL_WARN);  		writeSome();  		ensure_message_contains(9, "three");  		ensure_message_contains(10, "error");  		ensure_message_contains(11, "four");  		ensure_message_count(12); -		 +  		LLError::setDefaultLevel(LLError::LEVEL_ERROR);  		writeSome();  		ensure_message_contains(12, "error");  		ensure_message_contains(13, "four");  		ensure_message_count(14); -		 +  		LLError::setDefaultLevel(LLError::LEVEL_NONE);  		writeSome();  		ensure_message_count(14); @@ -218,14 +224,14 @@ namespace tut  	{  		std::string thisFile = __FILE__;  		std::string abbreviateFile = LLError::abbreviateFile(thisFile); -		 +  		ensure_ends_with("file name abbreviation",  			abbreviateFile,  			"llcommon/tests/llerror_test.cpp"  			);  		ensure_does_not_contain("file name abbreviation",  			abbreviateFile, "indra"); -			 +  		std::string someFile =  #if LL_WINDOWS  			"C:/amy/bob/cam.cpp" @@ -234,12 +240,12 @@ namespace tut  #endif  			;  		std::string someAbbreviation = LLError::abbreviateFile(someFile); -		 +  		ensure_equals("non-indra file abbreviation",  			someAbbreviation, someFile);  	}  } -	 +  namespace  {  	std::string locationString(int line) @@ -247,22 +253,22 @@ namespace  		std::ostringstream location;  		location << LLError::abbreviateFile(__FILE__)  				 << "(" << line << ") : "; -				  +  		return location.str();  	} -	 +  	std::string writeReturningLocation()  	{  		llinfos << "apple" << llendl;	int this_line = __LINE__;  		return locationString(this_line);  	} -	 +  	std::string writeReturningLocationAndFunction()  	{  		llinfos << "apple" << llendl;	int this_line = __LINE__;  		return locationString(this_line) + __FUNCTION__;  	} -	 +  	std::string errorReturningLocation()  	{  		llerrs << "die" << llendl;	int this_line = __LINE__; @@ -271,20 +277,20 @@ namespace  }  namespace tut -{	 +{  	template<> template<>  	void ErrorTestObject::test<5>()  		// file and line information in log messages  	{  		std::string location = writeReturningLocation();  			// expecting default to not print location information -		 +  		LLError::setPrintLocation(true);  		writeReturningLocation(); -		 +  		LLError::setPrintLocation(false);  		writeReturningLocation(); -		 +  		ensure_message_does_not_contain(0, location);  		ensure_message_contains(1, location);  		ensure_message_does_not_contain(2, location); @@ -297,7 +303,7 @@ namespace tut  	existing log messages often do.)  The functions all return their C++  	name so that test can be substantial mechanized.   */ -  +  std::string logFromGlobal(bool id)  {  	llinfos << (id ? "logFromGlobal: " : "") << "hi" << llendl; @@ -345,7 +351,7 @@ namespace  			return "ClassWithNoLogType::logFromStatic";  		}  	}; -	 +  	class ClassWithLogType {  		LOG_CLASS(ClassWithLogType);  	public: @@ -360,13 +366,13 @@ namespace  			return "ClassWithLogType::logFromStatic";  		}  	}; -	 +  	std::string logFromNamespace(bool id) { return Foo::logFromNamespace(id); }  	std::string logFromClassWithNoLogTypeMember(bool id) { ClassWithNoLogType c; return c.logFromMember(id); }  	std::string logFromClassWithNoLogTypeStatic(bool id) { return ClassWithNoLogType::logFromStatic(id); }  	std::string logFromClassWithLogTypeMember(bool id) { ClassWithLogType c; return c.logFromMember(id); }  	std::string logFromClassWithLogTypeStatic(bool id) { return ClassWithLogType::logFromStatic(id); } -	 +  	void ensure_has(const std::string& message,  		const std::string& actual, const std::string& expected)  	{ @@ -379,18 +385,18 @@ namespace  			throw tut::failure(ss.str().c_str());  		}  	} -	 +  	typedef std::string (*LogFromFunction)(bool); -	void testLogName(tut::TestRecorder& recorder, LogFromFunction f, +	void testLogName(tut::TestRecorder* recorder, LogFromFunction f,  		const std::string& class_name = "")  	{ -		recorder.clearMessages(); +		recorder->clearMessages();  		std::string name = f(false);  		f(true); -		 -		std::string messageWithoutName = recorder.message(0); -		std::string messageWithName = recorder.message(1); -		 + +		std::string messageWithoutName = recorder->message(0); +		std::string messageWithName = recorder->message(1); +  		ensure_has(name + " logged without name",  			messageWithoutName, name);  		ensure_has(name + " logged with name", @@ -431,18 +437,18 @@ namespace  		llinfos << "inside" << llendl;  		return "moo";  	} -	 +  	std::string outerLogger()  	{  		llinfos << "outside(" << innerLogger() << ")" << llendl;  		return "bar";  	} -	 +  	void uberLogger()  	{  		llinfos << "uber(" << outerLogger() << "," << innerLogger() << ")" << llendl;  	} -	 +  	class LogWhileLogging  	{  	public: @@ -461,11 +467,11 @@ namespace  		LogWhileLogging l;  		llinfos << "meta(" << l << ")" << llendl;  	} -	 +  }  namespace tut -{			 +{  	template<> template<>  		// handle nested logging  	void ErrorTestObject::test<7>() @@ -474,31 +480,31 @@ namespace tut  		ensure_message_contains(0, "inside");  		ensure_message_contains(1, "outside(moo)");  		ensure_message_count(2); -		 +  		uberLogger();  		ensure_message_contains(2, "inside");  		ensure_message_contains(3, "inside");  		ensure_message_contains(4, "outside(moo)");  		ensure_message_contains(5, "uber(bar,moo)");  		ensure_message_count(6); -		 +  		metaLogger();  		ensure_message_contains(6, "logging");  		ensure_message_contains(7, "meta(baz)");  		ensure_message_count(8);  	} -	 +  	template<> template<>  		// special handling of llerrs calls  	void ErrorTestObject::test<8>()  	{  		LLError::setPrintLocation(false);  		std::string location = errorReturningLocation(); -		 +  		ensure_message_contains(0, location + "error");  		ensure_message_contains(1, "die");  		ensure_message_count(2); -		 +  		ensure("fatal callback called", fatalWasCalled);  	}  } @@ -509,7 +515,7 @@ namespace  	{  		return "1947-07-08T03:04:05Z";  	} -	 +  	void ufoSighting()  	{  		llinfos << "ufo" << llendl; @@ -517,35 +523,35 @@ namespace  }  namespace tut -{	 +{  	template<> template<>  		// time in output (for recorders that need it)  	void ErrorTestObject::test<9>()  	{  		LLError::setTimeFunction(roswell); -		mRecorder.setWantsTime(false); +		mRecorder->setWantsTime(false);  		ufoSighting();  		ensure_message_contains(0, "ufo");  		ensure_message_does_not_contain(0, roswell()); -		 -		mRecorder.setWantsTime(true); + +		mRecorder->setWantsTime(true);  		ufoSighting();  		ensure_message_contains(1, "ufo");  		ensure_message_contains(1, roswell());  	} -	 +  	template<> template<>  		// output order  	void ErrorTestObject::test<10>()  	{  		LLError::setPrintLocation(true);  		LLError::setTimeFunction(roswell); -		mRecorder.setWantsTime(true); +		mRecorder->setWantsTime(true);  		std::string locationAndFunction = writeReturningLocationAndFunction(); -		 +  		ensure_equals("order is time type location function message", -			mRecorder.message(0), +			mRecorder->message(0),  			roswell() + " INFO: " + locationAndFunction + ": apple");  	} @@ -553,30 +559,30 @@ namespace tut  		// multiple recorders  	void ErrorTestObject::test<11>()  	{ -		TestRecorder altRecorder; -		LLError::addRecorder(&altRecorder); -		 +		TestRecorder* altRecorder(new TestRecorder); +		LLError::addRecorder(altRecorder); +  		llinfos << "boo" << llendl;  		ensure_message_contains(0, "boo"); -		ensure_equals("alt recorder count", altRecorder.countMessages(), 1); -		ensure_contains("alt recorder message 0", altRecorder.message(0), "boo"); -		 +		ensure_equals("alt recorder count", altRecorder->countMessages(), 1); +		ensure_contains("alt recorder message 0", altRecorder->message(0), "boo"); +  		LLError::setTimeFunction(roswell); -		TestRecorder anotherRecorder; -		anotherRecorder.setWantsTime(true); -		LLError::addRecorder(&anotherRecorder); -		 +		TestRecorder* anotherRecorder(new TestRecorder); +		anotherRecorder->setWantsTime(true); +		LLError::addRecorder(anotherRecorder); +  		llinfos << "baz" << llendl;  		std::string when = roswell(); -		 +  		ensure_message_does_not_contain(1, when); -		ensure_equals("alt recorder count", altRecorder.countMessages(), 2); -		ensure_does_not_contain("alt recorder message 1", altRecorder.message(1), when); -		ensure_equals("another recorder count", anotherRecorder.countMessages(), 1); -		ensure_contains("another recorder message 0", anotherRecorder.message(0), when); +		ensure_equals("alt recorder count", altRecorder->countMessages(), 2); +		ensure_does_not_contain("alt recorder message 1", altRecorder->message(1), when); +		ensure_equals("another recorder count", anotherRecorder->countMessages(), 1); +		ensure_contains("another recorder message 0", anotherRecorder->message(0), when);  	}  } @@ -610,10 +616,10 @@ namespace tut  	{  		LLError::setDefaultLevel(LLError::LEVEL_WARN);  		LLError::setClassLevel("TestBeta", LLError::LEVEL_INFO); -		 +  		TestAlpha::doAll();  		TestBeta::doAll(); -		 +  		ensure_message_contains(0, "aim west");  		ensure_message_contains(1, "error");  		ensure_message_contains(2, "ate eels"); @@ -623,7 +629,7 @@ namespace tut  		ensure_message_contains(6, "big easy");  		ensure_message_count(7);  	} -	 +  	template<> template<>  		// filtering by function, and that it will override class filtering  	void ErrorTestObject::test<13>() @@ -632,13 +638,13 @@ namespace tut  		LLError::setClassLevel("TestBeta", LLError::LEVEL_WARN);  		LLError::setFunctionLevel("TestBeta::doInfo", LLError::LEVEL_DEBUG);  		LLError::setFunctionLevel("TestBeta::doError", LLError::LEVEL_NONE); -		 +  		TestBeta::doAll();  		ensure_message_contains(0, "buy iron");  		ensure_message_contains(1, "bad word");  		ensure_message_count(2);  	} -	 +  	template<> template<>  		// filtering by file  		// and that it is overridden by both class and function filtering @@ -652,7 +658,7 @@ namespace tut  									LLError::LEVEL_NONE);  		LLError::setFunctionLevel("TestBeta::doError",  									LLError::LEVEL_NONE); -		 +  		TestAlpha::doAll();  		TestBeta::doAll();  		ensure_message_contains(0, "any idea"); @@ -660,7 +666,7 @@ namespace tut  		ensure_message_contains(2, "bad word");  		ensure_message_count(3);  	} -	 +  	template<> template<>  		// proper cached, efficient lookup of filtering  	void ErrorTestObject::test<15>() @@ -690,7 +696,7 @@ namespace tut  		ensure_message_count(2);  		ensure_equals("sixth check", LLError::shouldLogCallCount(), 3);  	} -	 +  	template<> template<>  		// configuration from LLSD  	void ErrorTestObject::test<16>() @@ -699,26 +705,26 @@ namespace tut  		LLSD config;  		config["print-location"] = true;  		config["default-level"] = "DEBUG"; -		 +  		LLSD set1;  		set1["level"] = "WARN";  		set1["files"][0] = this_file; -		 +  		LLSD set2;  		set2["level"] = "INFO";  		set2["classes"][0] = "TestAlpha"; -		 +  		LLSD set3;  		set3["level"] = "NONE";  		set3["functions"][0] = "TestAlpha::doError";  		set3["functions"][1] = "TestBeta::doError"; -		 +  		config["settings"][0] = set1;  		config["settings"][1] = set2;  		config["settings"][2] = set3; -		 +  		LLError::configure(config); -		 +  		TestAlpha::doAll();  		TestBeta::doAll();  		ensure_message_contains(0, "any idea"); @@ -726,13 +732,13 @@ namespace tut  		ensure_message_contains(1, "aim west");  		ensure_message_contains(2, "bad word");  		ensure_message_count(3); -		 +  		// make sure reconfiguring works  		LLSD config2;  		config2["default-level"] = "WARN"; -		 +  		LLError::configure(config2); -		 +  		TestAlpha::doAll();  		TestBeta::doAll();  		ensure_message_contains(3, "aim west"); @@ -744,13 +750,13 @@ namespace tut  		ensure_message_contains(8, "big easy");  		ensure_message_count(9);  	} -}	 +}  /* Tests left:  	handling of classes without LOG_CLASS -	live update of filtering from file	 -	 +	live update of filtering from file +  	syslog recorder  	file recorder  	cerr/stderr recorder diff --git a/indra/llcommon/tests/llinstancetracker_test.cpp b/indra/llcommon/tests/llinstancetracker_test.cpp index b34d1c5fd3..454695ff9f 100644 --- a/indra/llcommon/tests/llinstancetracker_test.cpp +++ b/indra/llcommon/tests/llinstancetracker_test.cpp @@ -35,6 +35,7 @@  #include <vector>  #include <set>  #include <algorithm>                // std::sort() +#include <stdexcept>  // std headers  // external library headers  #include <boost/scoped_ptr.hpp> @@ -42,6 +43,11 @@  #include "../test/lltut.h"  #include "wrapllerrs.h" +struct Badness: public std::runtime_error +{ +    Badness(const std::string& what): std::runtime_error(what) {} +}; +  struct Keyed: public LLInstanceTracker<Keyed, std::string>  {      Keyed(const std::string& name): @@ -53,6 +59,17 @@ struct Keyed: public LLInstanceTracker<Keyed, std::string>  struct Unkeyed: public LLInstanceTracker<Unkeyed>  { +    Unkeyed(const std::string& thrw="") +    { +        // LLInstanceTracker should respond appropriately if a subclass +        // constructor throws an exception. Specifically, it should run +        // LLInstanceTracker's destructor and remove itself from the +        // underlying container. +        if (! thrw.empty()) +        { +            throw Badness(thrw); +        } +    }  };  /***************************************************************************** @@ -95,6 +112,7 @@ namespace tut      void object::test<2>()      {          ensure_equals(Unkeyed::instanceCount(), 0); +        Unkeyed* dangling = NULL;          {              Unkeyed one;              ensure_equals(Unkeyed::instanceCount(), 1); @@ -107,7 +125,11 @@ namespace tut                  ensure_equals(found, two.get());              }              ensure_equals(Unkeyed::instanceCount(), 1); -        } +            // store an unwise pointer to a temp Unkeyed instance +            dangling = &one; +        } // make that instance vanish +        // check the now-invalid pointer to the destroyed instance +        ensure("getInstance(T*) failed to track destruction", ! Unkeyed::getInstance(dangling));          ensure_equals(Unkeyed::instanceCount(), 0);      } @@ -229,4 +251,49 @@ namespace tut          }          ensure(! what.empty());      } + +    template<> template<> +    void object::test<8>() +    { +        set_test_name("exception in subclass ctor"); +        typedef std::set<Unkeyed*> InstanceSet; +        InstanceSet existing; +        // We can't use the iterator-range InstanceSet constructor because +        // beginInstances() returns an iterator that dereferences to an +        // Unkeyed&, not an Unkeyed*. +        for (Unkeyed::instance_iter uki(Unkeyed::beginInstances()), +                                    ukend(Unkeyed::endInstances()); +             uki != ukend; ++uki) +        { +            existing.insert(&*uki); +        } +        Unkeyed* puk = NULL; +        try +        { +            // We don't expect the assignment to take place because we expect +            // Unkeyed to respond to the non-empty string param by throwing. +            // We know the LLInstanceTracker base-class constructor will have +            // run before Unkeyed's constructor, therefore the new instance +            // will have added itself to the underlying set. The whole +            // question is, when Unkeyed's constructor throws, will +            // LLInstanceTracker's destructor remove it from the set? I +            // realize we're testing the C++ implementation more than +            // Unkeyed's implementation, but this seems an important point to +            // nail down. +            puk = new Unkeyed("throw"); +        } +        catch (const Badness&) +        { +        } +        // Ensure that every member of the new, updated set of Unkeyed +        // instances was also present in the original set. If that's not true, +        // it's because our new Unkeyed ended up in the updated set despite +        // its constructor exception. +        for (Unkeyed::instance_iter uki(Unkeyed::beginInstances()), +                                    ukend(Unkeyed::endInstances()); +             uki != ukend; ++uki) +        { +            ensure("failed to remove instance", existing.find(&*uki) != existing.end()); +        } +    }  } // namespace tut diff --git a/indra/llcommon/tests/llleap_test.cpp b/indra/llcommon/tests/llleap_test.cpp new file mode 100644 index 0000000000..9b755e9ca5 --- /dev/null +++ b/indra/llcommon/tests/llleap_test.cpp @@ -0,0 +1,694 @@ +/** + * @file   llleap_test.cpp + * @author Nat Goodspeed + * @date   2012-02-21 + * @brief  Test for llleap. + *  + * $LicenseInfo:firstyear=2012&license=viewerlgpl$ + * Copyright (c) 2012, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "llleap.h" +// STL headers +// std headers +// external library headers +#include <boost/assign/list_of.hpp> +#include <boost/lambda/lambda.hpp> +#include <boost/foreach.hpp> +// other Linden headers +#include "../test/lltut.h" +#include "../test/namedtempfile.h" +#include "../test/manageapr.h" +#include "../test/catch_and_store_what_in.h" +#include "wrapllerrs.h" +#include "llevents.h" +#include "llprocess.h" +#include "stringize.h" +#include "StringVec.h" +#include <functional> + +using boost::assign::list_of; + +static ManageAPR manager; + +StringVec sv(const StringVec& listof) { return listof; } + +#if defined(LL_WINDOWS) +#define sleep(secs) _sleep((secs) * 1000) +#endif + +#if ! LL_WINDOWS +const size_t BUFFERED_LENGTH = 1023*1024; // try wrangling just under a megabyte of data +#else +// "Then there's Windows... sigh." The "very large message" test is flaky in a +// way that seems to point to either the OS (nonblocking writes to pipes) or +// possibly the apr_file_write() function. Poring over log messages reveals +// that at some point along the way apr_file_write() returns 11 (Resource +// temporarily unavailable, i.e. EAGAIN) and says it wrote 0 bytes -- even +// though it did write the chunk! Our next write attempt retries the same +// chunk, resulting in the chunk being duplicated at the child end, corrupting +// the data stream. Much as I would love to be able to fix it for real, such a +// fix would appear to require distinguishing bogus EAGAIN returns from real +// ones -- how?? Empirically this behavior is only observed when writing a +// "very large message". To be able to move forward at all, try to bypass this +// particular failure by adjusting the size of a "very large message" on +// Windows. +const size_t BUFFERED_LENGTH = 65336; +#endif  // LL_WINDOWS + +void waitfor(const std::vector<LLLeap*>& instances, int timeout=60) +{ +    int i; +    for (i = 0; i < timeout; ++i) +    { +        // Every iteration, test whether any of the passed LLLeap instances +        // still exist (are still running). +        std::vector<LLLeap*>::const_iterator vli(instances.begin()), vlend(instances.end()); +        for ( ; vli != vlend; ++vli) +        { +            // getInstance() returns NULL if it's terminated/gone, non-NULL if +            // it's still running +            if (LLLeap::getInstance(*vli)) +                break; +        } +        // If we made it through all of 'instances' without finding one that's +        // still running, we're done. +        if (vli == vlend) +        { +/*==========================================================================*| +            std::cout << instances.size() << " LLLeap instances terminated in " +                      << i << " seconds, proceeding" << std::endl; +|*==========================================================================*/ +            return; +        } +        // Found an instance that's still running. Wait and pump LLProcess. +        sleep(1); +        LLEventPumps::instance().obtain("mainloop").post(LLSD()); +    } +    tut::ensure(STRINGIZE("at least 1 of " << instances.size() +                          << " LLLeap instances timed out (" +                          << timeout << " seconds) without terminating"), +                i < timeout); +} + +void waitfor(LLLeap* instance, int timeout=60) +{ +    std::vector<LLLeap*> instances; +    instances.push_back(instance); +    waitfor(instances, timeout); +} + +/***************************************************************************** +*   TUT +*****************************************************************************/ +namespace tut +{ +    struct llleap_data +    { +        llleap_data(): +            reader(".py", +                   // This logic is adapted from vita.viewerclient.receiveEvent() +                   boost::lambda::_1 << +                   "import re\n" +                   "import os\n" +                   "import sys\n" +                   "\n" +                   // Don't forget that this Python script is written to some +                   // temp directory somewhere! Its __file__ is useless in +                   // finding indra/lib/python. Use our __FILE__, with +                   // raw-string syntax to deal with Windows pathnames. +                   "mydir = os.path.dirname(r'" << __FILE__ << "')\n" +                   "try:\n" +                   "    from llbase import llsd\n" +                   "except ImportError:\n" +                   // We expect mydir to be .../indra/llcommon/tests. +                   "    sys.path.insert(0,\n" +                   "        os.path.join(mydir, os.pardir, os.pardir, 'lib', 'python'))\n" +                   "    from indra.base import llsd\n" +                   "\n" +                   "class ProtocolError(Exception):\n" +                   "    def __init__(self, msg, data):\n" +                   "        Exception.__init__(self, msg)\n" +                   "        self.data = data\n" +                   "\n" +                   "class ParseError(ProtocolError):\n" +                   "    pass\n" +                   "\n" +                   "def get():\n" +                   "    hdr = ''\n" +                   "    while ':' not in hdr and len(hdr) < 20:\n" +                   "        hdr += sys.stdin.read(1)\n" +                   "        if not hdr:\n" +                   "            sys.exit(0)\n" +                   "    if not hdr.endswith(':'):\n" +                   "        raise ProtocolError('Expected len:data, got %r' % hdr, hdr)\n" +                   "    try:\n" +                   "        length = int(hdr[:-1])\n" +                   "    except ValueError:\n" +                   "        raise ProtocolError('Non-numeric len %r' % hdr[:-1], hdr[:-1])\n" +                   "    parts = []\n" +                   "    received = 0\n" +                   "    while received < length:\n" +                   "        parts.append(sys.stdin.read(length - received))\n" +                   "        received += len(parts[-1])\n" +                   "    data = ''.join(parts)\n" +                   "    assert len(data) == length\n" +                   "    try:\n" +                   "        return llsd.parse(data)\n" +                   //   Seems the old indra.base.llsd module didn't properly +                   //   convert IndexError (from running off end of string) to +                   //   LLSDParseError. +                   "    except (IndexError, llsd.LLSDParseError), e:\n" +                   "        msg = 'Bad received packet (%s)' % e\n" +                   "        print >>sys.stderr, '%s, %s bytes:' % (msg, len(data))\n" +                   "        showmax = 40\n" +                   //       We've observed failures with very large packets; +                   //       dumping the entire packet wastes time and space. +                   //       But if the error states a particular byte offset, +                   //       truncate to (near) that offset when dumping data. +                   "        location = re.search(r' at (byte|index) ([0-9]+)', str(e))\n" +                   "        if not location:\n" +                   "            # didn't find offset, dump whole thing, no ellipsis\n" +                   "            ellipsis = ''\n" +                   "        else:\n" +                   "            # found offset within error message\n" +                   "            trunc = int(location.group(2)) + showmax\n" +                   "            data = data[:trunc]\n" +                   "            ellipsis = '... (%s more)' % (length - trunc)\n" +                   "        offset = -showmax\n" +                   "        for offset in xrange(0, len(data)-showmax, showmax):\n" +                   "            print >>sys.stderr, '%04d: %r +' % \\\n" +                   "                  (offset, data[offset:offset+showmax])\n" +                   "        offset += showmax\n" +                   "        print >>sys.stderr, '%04d: %r%s' % \\\n" +                   "              (offset, data[offset:], ellipsis)\n" +                   "        raise ParseError(msg, data)\n" +                   "\n" +                   "# deal with initial stdin message\n" +                   // this will throw if the initial write to stdin doesn't +                   // follow len:data protocol, or if we couldn't find 'pump' +                   // in the dict +                   "_reply = get()['pump']\n" +                   "\n" +                   "def replypump():\n" +                   "    return _reply\n" +                   "\n" +                   "def put(req):\n" +                   "    sys.stdout.write(':'.join((str(len(req)), req)))\n" +                   "    sys.stdout.flush()\n" +                   "\n" +                   "def send(pump, data):\n" +                   "    put(llsd.format_notation(dict(pump=pump, data=data)))\n" +                   "\n" +                   "def request(pump, data):\n" +                   "    # we expect 'data' is a dict\n" +                   "    data['reply'] = _reply\n" +                   "    send(pump, data)\n"), +            // Get the actual pathname of the NamedExtTempFile and trim off +            // the ".py" extension. (We could cache reader.getName() in a +            // separate member variable, but I happen to know getName() just +            // returns a NamedExtTempFile member rather than performing any +            // computation, so I don't mind calling it twice.) Then take the +            // basename. +            reader_module(LLProcess::basename( +                              reader.getName().substr(0, reader.getName().length()-3))), +            pPYTHON(getenv("PYTHON")), +            PYTHON(pPYTHON? pPYTHON : "") +        { +            ensure("Set PYTHON to interpreter pathname", pPYTHON); +        } +        NamedExtTempFile reader; +        const std::string reader_module; +        const char* pPYTHON; +        const std::string PYTHON; +    }; +    typedef test_group<llleap_data> llleap_group; +    typedef llleap_group::object object; +    llleap_group llleapgrp("llleap"); + +    template<> template<> +    void object::test<1>() +    { +        set_test_name("multiple LLLeap instances"); +        NamedTempFile script("py", +                             "import time\n" +                             "time.sleep(1)\n"); +        std::vector<LLLeap*> instances; +        instances.push_back(LLLeap::create(get_test_name(), +                                           sv(list_of(PYTHON)(script.getName())))); +        instances.push_back(LLLeap::create(get_test_name(), +                                           sv(list_of(PYTHON)(script.getName())))); +        // In this case we're simply establishing that two LLLeap instances +        // can coexist without throwing exceptions or bombing in any other +        // way. Wait for them to terminate. +        waitfor(instances); +    } + +    template<> template<> +    void object::test<2>() +    { +        set_test_name("stderr to log"); +        NamedTempFile script("py", +                             "import sys\n" +                             "sys.stderr.write('''Hello from Python!\n" +                             "note partial line''')\n"); +        CaptureLog log(LLError::LEVEL_INFO); +        waitfor(LLLeap::create(get_test_name(), +                               sv(list_of(PYTHON)(script.getName())))); +        log.messageWith("Hello from Python!"); +        log.messageWith("note partial line"); +    } + +    template<> template<> +    void object::test<3>() +    { +        set_test_name("bad stdout protocol"); +        NamedTempFile script("py", +                             "print 'Hello from Python!'\n"); +        CaptureLog log(LLError::LEVEL_WARN); +        waitfor(LLLeap::create(get_test_name(), +                               sv(list_of(PYTHON)(script.getName())))); +        ensure_contains("error log line", +                        log.messageWith("invalid protocol"), "Hello from Python!"); +    } + +    template<> template<> +    void object::test<4>() +    { +        set_test_name("leftover stdout"); +        NamedTempFile script("py", +                             "import sys\n" +                             // note lack of newline +                             "sys.stdout.write('Hello from Python!')\n"); +        CaptureLog log(LLError::LEVEL_WARN); +        waitfor(LLLeap::create(get_test_name(), +                               sv(list_of(PYTHON)(script.getName())))); +        ensure_contains("error log line", +                        log.messageWith("Discarding"), "Hello from Python!"); +    } + +    template<> template<> +    void object::test<5>() +    { +        set_test_name("bad stdout len prefix"); +        NamedTempFile script("py", +                             "import sys\n" +                             "sys.stdout.write('5a2:something')\n"); +        CaptureLog log(LLError::LEVEL_WARN); +        waitfor(LLLeap::create(get_test_name(), +                               sv(list_of(PYTHON)(script.getName())))); +        ensure_contains("error log line", +                        log.messageWith("invalid protocol"), "5a2:"); +    } + +    template<> template<> +    void object::test<6>() +    { +        set_test_name("empty plugin vector"); +        std::string threw; +        try +        { +            LLLeap::create("empty", StringVec()); +        } +        CATCH_AND_STORE_WHAT_IN(threw, LLLeap::Error) +        ensure_contains("LLLeap::Error", threw, "no plugin"); +        // try the suppress-exception variant +        ensure("bad launch returned non-NULL", ! LLLeap::create("empty", StringVec(), false)); +    } + +    template<> template<> +    void object::test<7>() +    { +        set_test_name("bad launch"); +        // Synthesize bogus executable name +        std::string BADPYTHON(PYTHON.substr(0, PYTHON.length()-1) + "x"); +        CaptureLog log; +        std::string threw; +        try +        { +            LLLeap::create("bad exe", BADPYTHON); +        } +        CATCH_AND_STORE_WHAT_IN(threw, LLLeap::Error) +        ensure_contains("LLLeap::create() didn't throw", threw, "failed"); +        log.messageWith("failed"); +        log.messageWith(BADPYTHON); +        // try the suppress-exception variant +        ensure("bad launch returned non-NULL", ! LLLeap::create("bad exe", BADPYTHON, false)); +    } + +    // Generic self-contained listener: derive from this and override its +    // call() method, then tell somebody to post on the pump named getName(). +    // Control will reach your call() override. +    struct ListenerBase +    { +        // Pass the pump name you want; will tweak for uniqueness. +        ListenerBase(const std::string& name): +            mPump(name, true) +        { +            mPump.listen(name, boost::bind(&ListenerBase::call, this, _1)); +        } + +        virtual ~ListenerBase() {}  // pacify MSVC + +        virtual bool call(const LLSD& request) +        { +            return false; +        } + +        LLEventPump& getPump() { return mPump; } +        const LLEventPump& getPump() const { return mPump; } + +        std::string getName() const { return mPump.getName(); } +        void post(const LLSD& data) { mPump.post(data); } + +        LLEventStream mPump; +    }; + +    // Mimic a dummy little LLEventAPI that merely sends a reply back to its +    // requester on the "reply" pump. +    struct AckAPI: public ListenerBase +    { +        AckAPI(): ListenerBase("AckAPI") {} + +        virtual bool call(const LLSD& request) +        { +            LLEventPumps::instance().obtain(request["reply"]).post("ack"); +            return false; +        } +    }; + +    // Give LLLeap script a way to post success/failure. +    struct Result: public ListenerBase +    { +        Result(): ListenerBase("Result") {} + +        virtual bool call(const LLSD& request) +        { +            mData = request; +            return false; +        } + +        void ensure() const +        { +            tut::ensure(std::string("never posted to ") + getName(), mData.isDefined()); +            // Post an empty string for success, non-empty string is failure message. +            tut::ensure(mData, mData.asString().empty()); +        } + +        LLSD mData; +    }; + +    template<> template<> +    void object::test<8>() +    { +        set_test_name("round trip"); +        AckAPI api; +        Result result; +        NamedTempFile script("py", +                             boost::lambda::_1 << +                             "from " << reader_module << " import *\n" +                             // make a request on our little API +                             "request(pump='" << api.getName() << "', data={})\n" +                             // wait for its response +                             "resp = get()\n" +                             "result = '' if resp == dict(pump=replypump(), data='ack')\\\n" +                             "            else 'bad: ' + str(resp)\n" +                             "send(pump='" << result.getName() << "', data=result)\n"); +        waitfor(LLLeap::create(get_test_name(), sv(list_of(PYTHON)(script.getName())))); +        result.ensure(); +    } + +    struct ReqIDAPI: public ListenerBase +    { +        ReqIDAPI(): ListenerBase("ReqIDAPI") {} + +        virtual bool call(const LLSD& request) +        { +            // free function from llevents.h +            sendReply(LLSD(), request); +            return false; +        } +    }; + +    template<> template<> +    void object::test<9>() +    { +        set_test_name("many small messages"); +        // It's not clear to me whether there's value in iterating many times +        // over a send/receive loop -- I don't think that will exercise any +        // interesting corner cases. This test first sends a large number of +        // messages, then receives all the responses. The intent is to ensure +        // that some of that data stream crosses buffer boundaries, loop +        // iterations etc. in OS pipes and the LLLeap/LLProcess implementation. +        ReqIDAPI api; +        Result result; +        NamedTempFile script("py", +                             boost::lambda::_1 << +                             "import sys\n" +                             "from " << reader_module << " import *\n" +                             // Note that since reader imports llsd, this +                             // 'import *' gets us llsd too. +                             "sample = llsd.format_notation(dict(pump='" << +                             api.getName() << "', data=dict(reqid=999999, reply=replypump())))\n" +                             // The whole packet has length prefix too: "len:data" +                             "samplen = len(str(len(sample))) + 1 + len(sample)\n" +                             // guess how many messages it will take to +                             // accumulate BUFFERED_LENGTH +                             "count = int(" << BUFFERED_LENGTH << "/samplen)\n" +                             "print >>sys.stderr, 'Sending %s requests' % count\n" +                             "for i in xrange(count):\n" +                             "    request('" << api.getName() << "', dict(reqid=i))\n" +                             // The assumption in this specific test that +                             // replies will arrive in the same order as +                             // requests is ONLY valid because the API we're +                             // invoking sends replies instantly. If the API +                             // had to wait for some external event before +                             // sending its reply, replies could arrive in +                             // arbitrary order, and we'd have to tick them +                             // off from a set. +                             "result = ''\n" +                             "for i in xrange(count):\n" +                             "    resp = get()\n" +                             "    if resp['data']['reqid'] != i:\n" +                             "        result = 'expected reqid=%s in %s' % (i, resp)\n" +                             "        break\n" +                             "send(pump='" << result.getName() << "', data=result)\n"); +        waitfor(LLLeap::create(get_test_name(), sv(list_of(PYTHON)(script.getName()))), +                300);               // needs more realtime than most tests +        result.ensure(); +    } + +    // This is the body of test<10>, extracted so we can run it over a number +    // of large-message sizes. +    void test_large_message(const std::string& PYTHON, const std::string& reader_module, +                            const std::string& test_name, size_t size) +    { +        ReqIDAPI api; +        Result result; +        NamedTempFile script("py", +                             boost::lambda::_1 << +                             "import sys\n" +                             "from " << reader_module << " import *\n" +                             // Generate a very large string value. +                             "desired = int(sys.argv[1])\n" +                             // 7 chars per item: 6 digits, 1 comma +                             "count = int((desired - 50)/7)\n" +                             "large = ''.join('%06d,' % i for i in xrange(count))\n" +                             // Pass 'large' as reqid because we know the API +                             // will echo reqid, and we want to receive it back. +                             "request('" << api.getName() << "', dict(reqid=large))\n" +                             "try:\n" +                             "    resp = get()\n" +                             "except ParseError, e:\n" +                             "    # try to find where e.data diverges from expectation\n" +                             // Normally we'd expect a 'pump' key in there, +                             // too, with value replypump(). But Python +                             // serializes keys in a different order than C++, +                             // so incoming data start with 'data'. +                             // Truthfully, though, if we get as far as 'pump' +                             // before we find a difference, something's very +                             // strange. +                             "    expect = llsd.format_notation(dict(data=dict(reqid=large)))\n" +                             "    chunk = 40\n" +                             "    for offset in xrange(0, max(len(e.data), len(expect)), chunk):\n" +                             "        if e.data[offset:offset+chunk] != \\\n" +                             "           expect[offset:offset+chunk]:\n" +                             "            print >>sys.stderr, 'Offset %06d: expect %r,\\n'\\\n" +                             "                                '                  get %r' %\\\n" +                             "                                (offset,\n" +                             "                                 expect[offset:offset+chunk],\n" +                             "                                 e.data[offset:offset+chunk])\n" +                             "            break\n" +                             "    else:\n" +                             "        print >>sys.stderr, 'incoming data matches expect?!'\n" +                             "    send('" << result.getName() << "', '%s: %s' % (e.__class__.__name__, e))\n" +                             "    sys.exit(1)\n" +                             "\n" +                             "echoed = resp['data']['reqid']\n" +                             "if echoed == large:\n" +                             "    send('" << result.getName() << "', '')\n" +                             "    sys.exit(0)\n" +                             // Here we know echoed did NOT match; try to find where +                             "for i in xrange(count):\n" +                             "    start = 7*i\n" +                             "    end   = 7*(i+1)\n" +                             "    if end > len(echoed)\\\n" +                             "    or echoed[start:end] != large[start:end]:\n" +                             "        send('" << result.getName() << "',\n" +                             "             'at offset %s, expected %r but got %r' %\n" +                             "             (start, large[start:end], echoed[start:end]))\n" +                             "sys.exit(1)\n"); +        waitfor(LLLeap::create(test_name, +                               sv(list_of +                                  (PYTHON) +                                  (script.getName()) +                                  (stringize(size)))), +                180);               // try a longer timeout +        result.ensure(); +    } + +    struct TestLargeMessage: public std::binary_function<size_t, size_t, bool> +    { +        TestLargeMessage(const std::string& PYTHON_, const std::string& reader_module_, +                         const std::string& test_name_): +            PYTHON(PYTHON_), +            reader_module(reader_module_), +            test_name(test_name_) +        {} + +        bool operator()(size_t left, size_t right) const +        { +            // We don't know whether upper_bound is going to pass the "sought +            // value" as the left or the right operand. We pass 0 as the +            // "sought value" so we can distinguish it. Of course that means +            // the sequence we're searching must not itself contain 0! +            size_t size; +            bool success; +            if (left) +            { +                size = left; +                // Consider our return value carefully. Normal binary_search +                // (or, in our case, upper_bound) expects a container sorted +                // in ascending order, and defaults to the std::less +                // comparator. Our container is in fact in ascending order, so +                // return consistently with std::less. Here we were called as +                // compare(item, sought). If std::less were called that way, +                // 'true' would mean to move right (to higher numbers) within +                // the sequence: the item being considered is less than the +                // sought value. For us, that means that test_large_message() +                // success should return 'true'. +                success = true; +            } +            else +            { +                size = right; +                // Here we were called as compare(sought, item). If std::less +                // were called that way, 'true' would mean to move left (to +                // lower numbers) within the sequence: the sought value is +                // less than the item being considered. For us, that means +                // test_large_message() FAILURE should return 'true', hence +                // test_large_message() success should return 'false'. +                success = false; +            } + +            try +            { +                test_large_message(PYTHON, reader_module, test_name, size); +                std::cout << "test_large_message(" << size << ") succeeded" << std::endl; +                return success; +            } +            catch (const failure& e) +            { +                std::cout << "test_large_message(" << size << ") failed: " << e.what() << std::endl; +                return ! success; +            } +        } + +        const std::string PYTHON, reader_module, test_name; +    }; + +    // The point of this function is to try to find a size at which +    // test_large_message() can succeed. We still want the overall test to +    // fail; otherwise we won't get the coder's attention -- but if +    // test_large_message() fails, try to find a plausible size at which it +    // DOES work. +    void test_or_split(const std::string& PYTHON, const std::string& reader_module, +                       const std::string& test_name, size_t size) +    { +        try +        { +            test_large_message(PYTHON, reader_module, test_name, size); +        } +        catch (const failure& e) +        { +            std::cout << "test_large_message(" << size << ") failed: " << e.what() << std::endl; +            // If it still fails below 4K, give up: subdividing any further is +            // pointless. +            if (size >= 4096) +            { +                try +                { +                    // Recur with half the size +                    size_t smaller(size/2); +                    test_or_split(PYTHON, reader_module, test_name, smaller); +                    // Recursive call will throw if test_large_message() +                    // failed, therefore we only reach the line below if it +                    // succeeded. +                    std::cout << "but test_large_message(" << smaller << ") succeeded" << std::endl; + +                    // Binary search for largest size that works. But since +                    // std::binary_search() only returns bool, actually use +                    // std::upper_bound(), consistent with our desire to find +                    // the LARGEST size that works. First generate a sorted +                    // container of all the sizes we intend to try, from +                    // 'smaller' (known to work) to 'size' (known to fail). We +                    // could whomp up magic iterators to do this dynamically, +                    // without actually instantiating a vector, but for a test +                    // program this will do. At least preallocate the vector. +                    // Per TestLargeMessage comments, it's important that this +                    // vector not contain 0. +                    std::vector<size_t> sizes; +                    sizes.reserve((size - smaller)/4096 + 1); +                    for (size_t sz(smaller), szend(size); sz < szend; sz += 4096) +                        sizes.push_back(sz); +                    // our comparator +                    TestLargeMessage tester(PYTHON, reader_module, test_name); +                    // Per TestLargeMessage comments, pass 0 as the sought value. +                    std::vector<size_t>::const_iterator found = +                        std::upper_bound(sizes.begin(), sizes.end(), 0, tester); +                    if (found != sizes.end() && found != sizes.begin()) +                    { +                        std::cout << "test_large_message(" << *(found - 1) +                                  << ") is largest that succeeds" << std::endl; +                    } +                    else +                    { +                        std::cout << "cannot determine largest test_large_message(size) " +                                  << "that succeeds" << std::endl; +                    } +                } +                catch (const failure&) +                { +                    // The recursive test_or_split() call above has already +                    // handled the exception. We don't want our caller to see +                    // innermost exception; propagate outermost (below). +                } +            } +            // In any case, because we reached here through failure of +            // our original test_large_message(size) call, ensure failure +            // propagates. +            throw e; +        } +    } + +    template<> template<> +    void object::test<10>() +    { +        set_test_name("very large message"); +        test_or_split(PYTHON, reader_module, get_test_name(), BUFFERED_LENGTH); +    } +} // namespace tut diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp new file mode 100644 index 0000000000..99186ed434 --- /dev/null +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -0,0 +1,1262 @@ +/** + * @file   llprocess_test.cpp + * @author Nat Goodspeed + * @date   2011-12-19 + * @brief  Test for llprocess. + *  + * $LicenseInfo:firstyear=2011&license=viewerlgpl$ + * Copyright (c) 2011, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "llprocess.h" +// STL headers +#include <vector> +#include <list> +// std headers +#include <fstream> +// external library headers +#include "llapr.h" +#include "apr_thread_proc.h" +#include <boost/foreach.hpp> +#include <boost/function.hpp> +#include <boost/algorithm/string/find_iterator.hpp> +#include <boost/algorithm/string/finder.hpp> +//#include <boost/lambda/lambda.hpp> +//#include <boost/lambda/bind.hpp> +// other Linden headers +#include "../test/lltut.h" +#include "../test/manageapr.h" +#include "../test/namedtempfile.h" +#include "../test/catch_and_store_what_in.h" +#include "stringize.h" +#include "llsdutil.h" +#include "llevents.h" +#include "wrapllerrs.h" + +#if defined(LL_WINDOWS) +#define sleep(secs) _sleep((secs) * 1000) +#define EOL "\r\n" +#else +#define EOL "\n" +#include <sys/wait.h> +#endif + +//namespace lambda = boost::lambda; + +// static instance of this manages APR init/cleanup +static ManageAPR manager; + +/***************************************************************************** +*   Helpers +*****************************************************************************/ + +#define ensure_equals_(left, right) \ +        ensure_equals(STRINGIZE(#left << " != " << #right), (left), (right)) + +#define aprchk(expr) aprchk_(#expr, (expr)) +static void aprchk_(const char* call, apr_status_t rv, apr_status_t expected=APR_SUCCESS) +{ +    tut::ensure_equals(STRINGIZE(call << " => " << rv << ": " << manager.strerror(rv)), +                       rv, expected); +} + +/** + * Read specified file using std::getline(). It is assumed to be an error if + * the file is empty: don't use this function if that's an acceptable case. + * Last line will not end with '\n'; this is to facilitate the usual case of + * string compares with a single line of output. + * @param pathname The file to read. + * @param desc Optional description of the file for error message; + * defaults to "in <pathname>" + */ +static std::string readfile(const std::string& pathname, const std::string& desc="") +{ +    std::string use_desc(desc); +    if (use_desc.empty()) +    { +        use_desc = STRINGIZE("in " << pathname); +    } +    std::ifstream inf(pathname.c_str()); +    std::string output; +    tut::ensure(STRINGIZE("No output " << use_desc), std::getline(inf, output)); +    std::string more; +    while (std::getline(inf, more)) +    { +        output += '\n' + more; +    } +    return output; +} + +/// Looping on LLProcess::isRunning() must now be accompanied by pumping +/// "mainloop" -- otherwise the status won't update and you get an infinite +/// loop. +void yield(int seconds=1) +{ +    // This function simulates waiting for another viewer frame +    sleep(seconds); +    LLEventPumps::instance().obtain("mainloop").post(LLSD()); +} + +void waitfor(LLProcess& proc, int timeout=60) +{ +    int i = 0; +    for ( ; i < timeout && proc.isRunning(); ++i) +    { +        yield(); +    } +    tut::ensure(STRINGIZE("process took longer than " << timeout << " seconds to terminate"), +                i < timeout); +} + +void waitfor(LLProcess::handle h, const std::string& desc, int timeout=60) +{ +    int i = 0; +    for ( ; i < timeout && LLProcess::isRunning(h, desc); ++i) +    { +        yield(); +    } +    tut::ensure(STRINGIZE("process took longer than " << timeout << " seconds to terminate"), +                i < timeout); +} + +/** + * Construct an LLProcess to run a Python script. + */ +struct PythonProcessLauncher +{ +    /** +     * @param desc Arbitrary description for error messages +     * @param script Python script, any form acceptable to NamedTempFile, +     * typically either a std::string or an expression of the form +     * (lambda::_1 << "script content with " << variable_data) +     */ +    template <typename CONTENT> +    PythonProcessLauncher(const std::string& desc, const CONTENT& script): +        mDesc(desc), +        mScript("py", script) +    { +        const char* PYTHON(getenv("PYTHON")); +        tut::ensure("Set $PYTHON to the Python interpreter", PYTHON); + +        mParams.desc = desc + " script"; +        mParams.executable = PYTHON; +        mParams.args.add(mScript.getName()); +    } + +    /// Launch Python script; verify that it launched +    void launch() +    { +        mPy = LLProcess::create(mParams); +        tut::ensure(STRINGIZE("Couldn't launch " << mDesc << " script"), mPy); +    } + +    /// Run Python script and wait for it to complete. +    void run() +    { +        launch(); +        // One of the irritating things about LLProcess is that +        // there's no API to wait for the child to terminate -- but given +        // its use in our graphics-intensive interactive viewer, it's +        // understandable. +        waitfor(*mPy); +    } + +    /** +     * Run a Python script using LLProcess, expecting that it will +     * write to the file passed as its sys.argv[1]. Retrieve that output. +     * +     * Until January 2012, LLProcess provided distressingly few +     * mechanisms for a child process to communicate back to its caller -- +     * not even its return code. We've introduced a convention by which we +     * create an empty temp file, pass the name of that file to our child +     * as sys.argv[1] and expect the script to write its output to that +     * file. This function implements the C++ (parent process) side of +     * that convention. +     */ +    std::string run_read() +    { +        NamedTempFile out("out", ""); // placeholder +        // pass name of this temporary file to the script +        mParams.args.add(out.getName()); +        run(); +        // assuming the script wrote to that file, read it +        return readfile(out.getName(), STRINGIZE("from " << mDesc << " script")); +    } + +    LLProcess::Params mParams; +    LLProcessPtr mPy; +    std::string mDesc; +    NamedTempFile mScript; +}; + +/// convenience function for PythonProcessLauncher::run() +template <typename CONTENT> +static void python(const std::string& desc, const CONTENT& script) +{ +    PythonProcessLauncher py(desc, script); +    py.run(); +} + +/// convenience function for PythonProcessLauncher::run_read() +template <typename CONTENT> +static std::string python_out(const std::string& desc, const CONTENT& script) +{ +    PythonProcessLauncher py(desc, script); +    return py.run_read(); +} + +/// Create a temporary directory and clean it up later. +class NamedTempDir: public boost::noncopyable +{ +public: +    // Use python() function to create a temp directory: I've found +    // nothing in either Boost.Filesystem or APR quite like Python's +    // tempfile.mkdtemp(). +    // Special extra bonus: on Mac, mkdtemp() reports a pathname +    // starting with /var/folders/something, whereas that's really a +    // symlink to /private/var/folders/something. Have to use +    // realpath() to compare properly. +    NamedTempDir(): +        mPath(python_out("mkdtemp()", +                         "from __future__ import with_statement\n" +                         "import os.path, sys, tempfile\n" +                         "with open(sys.argv[1], 'w') as f:\n" +                         "    f.write(os.path.normcase(os.path.normpath(os.path.realpath(tempfile.mkdtemp()))))\n")) +    {} + +    ~NamedTempDir() +    { +        aprchk(apr_dir_remove(mPath.c_str(), gAPRPoolp)); +    } + +    std::string getName() const { return mPath; } + +private: +    std::string mPath; +}; + +/***************************************************************************** +*   TUT +*****************************************************************************/ +namespace tut +{ +    struct llprocess_data +    { +        LLAPRPool pool; +    }; +    typedef test_group<llprocess_data> llprocess_group; +    typedef llprocess_group::object object; +    llprocess_group llprocessgrp("llprocess"); + +    struct Item +    { +        Item(): tries(0) {} +        unsigned    tries; +        std::string which; +        std::string what; +    }; + +/*==========================================================================*| +#define tabent(symbol) { symbol, #symbol } +    static struct ReasonCode +    { +        int code; +        const char* name; +    } reasons[] = +    { +        tabent(APR_OC_REASON_DEATH), +        tabent(APR_OC_REASON_UNWRITABLE), +        tabent(APR_OC_REASON_RESTART), +        tabent(APR_OC_REASON_UNREGISTER), +        tabent(APR_OC_REASON_LOST), +        tabent(APR_OC_REASON_RUNNING) +    }; +#undef tabent +|*==========================================================================*/ + +    struct WaitInfo +    { +        WaitInfo(apr_proc_t* child_): +            child(child_), +            rv(-1),                 // we haven't yet called apr_proc_wait() +            rc(0), +            why(apr_exit_why_e(0)) +        {} +        apr_proc_t* child;          // which subprocess +        apr_status_t rv;            // return from apr_proc_wait() +        int rc;                     // child's exit code +        apr_exit_why_e why;         // APR_PROC_EXIT, APR_PROC_SIGNAL, APR_PROC_SIGNAL_CORE +    }; + +    void child_status_callback(int reason, void* data, int status) +    { +/*==========================================================================*| +        std::string reason_str; +        BOOST_FOREACH(const ReasonCode& rcp, reasons) +        { +            if (reason == rcp.code) +            { +                reason_str = rcp.name; +                break; +            } +        } +        if (reason_str.empty()) +        { +            reason_str = STRINGIZE("unknown reason " << reason); +        } +        std::cout << "child_status_callback(" << reason_str << ")\n"; +|*==========================================================================*/ + +        if (reason == APR_OC_REASON_DEATH || reason == APR_OC_REASON_LOST) +        { +            // Somewhat oddly, APR requires that you explicitly unregister +            // even when it already knows the child has terminated. +            apr_proc_other_child_unregister(data); + +            WaitInfo* wi(static_cast<WaitInfo*>(data)); +            // It's just wrong to call apr_proc_wait() here. The only way APR +            // knows to call us with APR_OC_REASON_DEATH is that it's already +            // reaped this child process, so calling wait() will only produce +            // "huh?" from the OS. We must rely on the status param passed in, +            // which unfortunately comes straight from the OS wait() call. +//          wi->rv = apr_proc_wait(wi->child, &wi->rc, &wi->why, APR_NOWAIT); +            wi->rv = APR_CHILD_DONE; // fake apr_proc_wait() results +#if defined(LL_WINDOWS) +            wi->why = APR_PROC_EXIT; +            wi->rc  = status;         // no encoding on Windows (no signals) +#else  // Posix +            if (WIFEXITED(status)) +            { +                wi->why = APR_PROC_EXIT; +                wi->rc  = WEXITSTATUS(status); +            } +            else if (WIFSIGNALED(status)) +            { +                wi->why = APR_PROC_SIGNAL; +                wi->rc  = WTERMSIG(status); +            } +            else                    // uh, shouldn't happen? +            { +                wi->why = APR_PROC_EXIT; +                wi->rc  = status;   // someone else will have to decode +            } +#endif // Posix +        } +    } + +    template<> template<> +    void object::test<1>() +    { +        set_test_name("raw APR nonblocking I/O"); + +        // Create a script file in a temporary place. +        NamedTempFile script("py", +            "import sys" EOL +            "import time" EOL +            EOL +            "time.sleep(2)" EOL +            "print >>sys.stdout, 'stdout after wait'" EOL +            "sys.stdout.flush()" EOL +            "time.sleep(2)" EOL +            "print >>sys.stderr, 'stderr after wait'" EOL +            "sys.stderr.flush()" EOL +            ); + +        // Arrange to track the history of our interaction with child: what we +        // fetched, which pipe it came from, how many tries it took before we +        // got it. +        std::vector<Item> history; +        history.push_back(Item()); + +        // Run the child process. +        apr_procattr_t *procattr = NULL; +        aprchk(apr_procattr_create(&procattr, pool.getAPRPool())); +        aprchk(apr_procattr_io_set(procattr, APR_CHILD_BLOCK, APR_CHILD_BLOCK, APR_CHILD_BLOCK)); +        aprchk(apr_procattr_cmdtype_set(procattr, APR_PROGRAM_PATH)); + +        std::vector<const char*> argv; +        apr_proc_t child; +        argv.push_back("python"); +        // Have to have a named copy of this std::string so its c_str() value +        // will persist. +        std::string scriptname(script.getName()); +        argv.push_back(scriptname.c_str()); +        argv.push_back(NULL); + +        aprchk(apr_proc_create(&child, argv[0], +                               &argv[0], +                               NULL, // if we wanted to pass explicit environment +                               procattr, +                               pool.getAPRPool())); + +        // We do not want this child process to outlive our APR pool. On +        // destruction of the pool, forcibly kill the process. Tell APR to try +        // SIGTERM and wait 3 seconds. If that didn't work, use SIGKILL. +        apr_pool_note_subprocess(pool.getAPRPool(), &child, APR_KILL_AFTER_TIMEOUT); + +        // arrange to call child_status_callback() +        WaitInfo wi(&child); +        apr_proc_other_child_register(&child, child_status_callback, &wi, child.in, pool.getAPRPool()); + +        // TODO: +        // Stuff child.in until it (would) block to verify EWOULDBLOCK/EAGAIN. +        // Have child script clear it later, then write one more line to prove +        // that it gets through. + +        // Monitor two different output pipes. Because one will be closed +        // before the other, keep them in a list so we can drop whichever of +        // them is closed first. +        typedef std::pair<std::string, apr_file_t*> DescFile; +        typedef std::list<DescFile> DescFileList; +        DescFileList outfiles; +        outfiles.push_back(DescFile("out", child.out)); +        outfiles.push_back(DescFile("err", child.err)); + +        while (! outfiles.empty()) +        { +            // This peculiar for loop is designed to let us erase(dfli). With +            // a list, that invalidates only dfli itself -- but even so, we +            // lose the ability to increment it for the next item. So at the +            // top of every loop, while dfli is still valid, increment +            // dflnext. Then before the next iteration, set dfli to dflnext. +            for (DescFileList::iterator +                     dfli(outfiles.begin()), dflnext(outfiles.begin()), dflend(outfiles.end()); +                 dfli != dflend; dfli = dflnext) +            { +                // Only valid to increment dflnext once we're sure it's not +                // already at dflend. +                ++dflnext; + +                char buf[4096]; + +                apr_status_t rv = apr_file_gets(buf, sizeof(buf), dfli->second); +                if (APR_STATUS_IS_EOF(rv)) +                { +//                  std::cout << "(EOF on " << dfli->first << ")\n"; +//                  history.back().which = dfli->first; +//                  history.back().what  = "*eof*"; +//                  history.push_back(Item()); +                    outfiles.erase(dfli); +                    continue; +                } +                if (rv == EWOULDBLOCK || rv == EAGAIN) +                { +//                  std::cout << "(waiting; apr_file_gets(" << dfli->first << ") => " << rv << ": " << manager.strerror(rv) << ")\n"; +                    ++history.back().tries; +                    continue; +                } +                aprchk_("apr_file_gets(buf, sizeof(buf), dfli->second)", rv); +                // Is it even possible to get APR_SUCCESS but read 0 bytes? +                // Hope not, but defend against that anyway. +                if (buf[0]) +                { +//                  std::cout << dfli->first << ": " << buf; +                    history.back().which = dfli->first; +                    history.back().what.append(buf); +                    if (buf[strlen(buf) - 1] == '\n') +                        history.push_back(Item()); +                    else +                    { +                        // Just for pretty output... if we only read a partial +                        // line, terminate it. +//                      std::cout << "...\n"; +                    } +                } +            } +            // Do this once per tick, as we expect the viewer will +            apr_proc_other_child_refresh_all(APR_OC_REASON_RUNNING); +            sleep(1); +        } +        apr_file_close(child.in); +        apr_file_close(child.out); +        apr_file_close(child.err); + +        // Okay, we've broken the loop because our pipes are all closed. If we +        // haven't yet called wait, give the callback one more chance. This +        // models the fact that unlike this small test program, the viewer +        // will still be running. +        if (wi.rv == -1) +        { +            std::cout << "last gasp apr_proc_other_child_refresh_all()\n"; +            apr_proc_other_child_refresh_all(APR_OC_REASON_RUNNING); +        } + +        if (wi.rv == -1) +        { +            std::cout << "child_status_callback(APR_OC_REASON_DEATH) wasn't called" << std::endl; +            wi.rv = apr_proc_wait(wi.child, &wi.rc, &wi.why, APR_NOWAIT); +        } +//      std::cout << "child done: rv = " << rv << " (" << manager.strerror(rv) << "), why = " << why << ", rc = " << rc << '\n'; +        aprchk_("apr_proc_wait(wi->child, &wi->rc, &wi->why, APR_NOWAIT)", wi.rv, APR_CHILD_DONE); +        ensure_equals_(wi.why, APR_PROC_EXIT); +        ensure_equals_(wi.rc, 0); + +        // Beyond merely executing all the above successfully, verify that we +        // obtained expected output -- and that we duly got control while +        // waiting, proving the non-blocking nature of these pipes. +        try +        { +            unsigned i = 0; +            ensure("blocking I/O on child pipe (0)", history[i].tries); +            ensure_equals_(history[i].which, "out"); +            ensure_equals_(history[i].what,  "stdout after wait" EOL); +//          ++i; +//          ensure_equals_(history[i].which, "out"); +//          ensure_equals_(history[i].what,  "*eof*"); +            ++i; +            ensure("blocking I/O on child pipe (1)", history[i].tries); +            ensure_equals_(history[i].which, "err"); +            ensure_equals_(history[i].what,  "stderr after wait" EOL); +//          ++i; +//          ensure_equals_(history[i].which, "err"); +//          ensure_equals_(history[i].what,  "*eof*"); +        } +        catch (const failure&) +        { +            std::cout << "History:\n"; +            BOOST_FOREACH(const Item& item, history) +            { +                std::string what(item.what); +                if ((! what.empty()) && what[what.length() - 1] == '\n') +                { +                    what.erase(what.length() - 1); +                    if ((! what.empty()) && what[what.length() - 1] == '\r') +                    { +                        what.erase(what.length() - 1); +                        what.append("\\r"); +                    } +                    what.append("\\n"); +                } +                std::cout << "  " << item.which << ": '" << what << "' (" +                          << item.tries << " tries)\n"; +            } +            std::cout << std::flush; +            // re-raise same error; just want to enrich the output +            throw; +        } +    } + +    template<> template<> +    void object::test<2>() +    { +        set_test_name("setWorkingDirectory()"); +        // We want to test setWorkingDirectory(). But what directory is +        // guaranteed to exist on every machine, under every OS? Have to +        // create one. Naturally, ensure we clean it up when done. +        NamedTempDir tempdir; +        PythonProcessLauncher py(get_test_name(), +                                 "from __future__ import with_statement\n" +                                 "import os, sys\n" +                                 "with open(sys.argv[1], 'w') as f:\n" +                                 "    f.write(os.path.normcase(os.path.normpath(os.getcwd())))\n"); +        // Before running, call setWorkingDirectory() +        py.mParams.cwd = tempdir.getName(); +        ensure_equals("os.getcwd()", py.run_read(), tempdir.getName()); +    } + +    template<> template<> +    void object::test<3>() +    { +        set_test_name("arguments"); +        PythonProcessLauncher py(get_test_name(), +                                 "from __future__ import with_statement\n" +                                 "import sys\n" +                                 // note nonstandard output-file arg! +                                 "with open(sys.argv[3], 'w') as f:\n" +                                 "    for arg in sys.argv[1:]:\n" +                                 "        print >>f, arg\n"); +        // We expect that PythonProcessLauncher has already appended +        // its own NamedTempFile to mParams.args (sys.argv[0]). +        py.mParams.args.add("first arg");          // sys.argv[1] +        py.mParams.args.add("second arg");         // sys.argv[2] +        // run_read() appends() one more argument, hence [3] +        std::string output(py.run_read()); +        boost::split_iterator<std::string::const_iterator> +            li(output, boost::first_finder("\n")), lend; +        ensure("didn't get first arg", li != lend); +        std::string arg(li->begin(), li->end()); +        ensure_equals(arg, "first arg"); +        ++li; +        ensure("didn't get second arg", li != lend); +        arg.assign(li->begin(), li->end()); +        ensure_equals(arg, "second arg"); +        ++li; +        ensure("didn't get output filename?!", li != lend); +        arg.assign(li->begin(), li->end()); +        ensure("output filename empty?!", ! arg.empty()); +        ++li; +        ensure("too many args", li == lend); +    } + +    template<> template<> +    void object::test<4>() +    { +        set_test_name("exit(0)"); +        PythonProcessLauncher py(get_test_name(), +                                 "import sys\n" +                                 "sys.exit(0)\n"); +        py.run(); +        ensure_equals("Status.mState", py.mPy->getStatus().mState, LLProcess::EXITED); +        ensure_equals("Status.mData",  py.mPy->getStatus().mData,  0); +    } + +    template<> template<> +    void object::test<5>() +    { +        set_test_name("exit(2)"); +        PythonProcessLauncher py(get_test_name(), +                                 "import sys\n" +                                 "sys.exit(2)\n"); +        py.run(); +        ensure_equals("Status.mState", py.mPy->getStatus().mState, LLProcess::EXITED); +        ensure_equals("Status.mData",  py.mPy->getStatus().mData,  2); +    } + +    template<> template<> +    void object::test<6>() +    { +        set_test_name("syntax_error:"); +        PythonProcessLauncher py(get_test_name(), +                                 "syntax_error:\n"); +        py.mParams.files.add(LLProcess::FileParam()); // inherit stdin +        py.mParams.files.add(LLProcess::FileParam()); // inherit stdout +        py.mParams.files.add(LLProcess::FileParam().type("pipe")); // pipe for stderr +        py.run(); +        ensure_equals("Status.mState", py.mPy->getStatus().mState, LLProcess::EXITED); +        ensure_equals("Status.mData",  py.mPy->getStatus().mData,  1); +        std::istream& rpipe(py.mPy->getReadPipe(LLProcess::STDERR).get_istream()); +        std::vector<char> buffer(4096); +        rpipe.read(&buffer[0], buffer.size()); +        std::streamsize got(rpipe.gcount()); +        ensure("Nothing read from stderr pipe", got); +        std::string data(&buffer[0], got); +        ensure("Didn't find 'SyntaxError:'", data.find("\nSyntaxError:") != std::string::npos); +    } + +    template<> template<> +    void object::test<7>() +    { +        set_test_name("explicit kill()"); +        PythonProcessLauncher py(get_test_name(), +                                 "from __future__ import with_statement\n" +                                 "import sys, time\n" +                                 "with open(sys.argv[1], 'w') as f:\n" +                                 "    f.write('ok')\n" +                                 "# now sleep; expect caller to kill\n" +                                 "time.sleep(120)\n" +                                 "# if caller hasn't managed to kill by now, bad\n" +                                 "with open(sys.argv[1], 'w') as f:\n" +                                 "    f.write('bad')\n"); +        NamedTempFile out("out", "not started"); +        py.mParams.args.add(out.getName()); +        py.launch(); +        // Wait for the script to wake up and do its first write +        int i = 0, timeout = 60; +        for ( ; i < timeout; ++i) +        { +            yield(); +            if (readfile(out.getName(), "from kill() script") == "ok") +                break; +        } +        // If we broke this loop because of the counter, something's wrong +        ensure("script never started", i < timeout); +        // script has performed its first write and should now be sleeping. +        py.mPy->kill(); +        // wait for the script to terminate... one way or another. +        waitfor(*py.mPy); +#if LL_WINDOWS +        ensure_equals("Status.mState", py.mPy->getStatus().mState, LLProcess::EXITED); +        ensure_equals("Status.mData",  py.mPy->getStatus().mData,  -1); +#else +        ensure_equals("Status.mState", py.mPy->getStatus().mState, LLProcess::KILLED); +        ensure_equals("Status.mData",  py.mPy->getStatus().mData,  SIGTERM); +#endif +        // If kill() failed, the script would have woken up on its own and +        // overwritten the file with 'bad'. But if kill() succeeded, it should +        // not have had that chance. +        ensure_equals(get_test_name() + " script output", readfile(out.getName()), "ok"); +    } + +    template<> template<> +    void object::test<8>() +    { +        set_test_name("implicit kill()"); +        NamedTempFile out("out", "not started"); +        LLProcess::handle phandle(0); +        { +            PythonProcessLauncher py(get_test_name(), +                                     "from __future__ import with_statement\n" +                                     "import sys, time\n" +                                     "with open(sys.argv[1], 'w') as f:\n" +                                     "    f.write('ok')\n" +                                     "# now sleep; expect caller to kill\n" +                                     "time.sleep(120)\n" +                                     "# if caller hasn't managed to kill by now, bad\n" +                                     "with open(sys.argv[1], 'w') as f:\n" +                                     "    f.write('bad')\n"); +            py.mParams.args.add(out.getName()); +            py.launch(); +            // Capture handle for later +            phandle = py.mPy->getProcessHandle(); +            // Wait for the script to wake up and do its first write +            int i = 0, timeout = 60; +            for ( ; i < timeout; ++i) +            { +                yield(); +                if (readfile(out.getName(), "from kill() script") == "ok") +                    break; +            } +            // If we broke this loop because of the counter, something's wrong +            ensure("script never started", i < timeout); +            // Script has performed its first write and should now be sleeping. +            // Destroy the LLProcess, which should kill the child. +        } +        // wait for the script to terminate... one way or another. +        waitfor(phandle, "kill() script"); +        // If kill() failed, the script would have woken up on its own and +        // overwritten the file with 'bad'. But if kill() succeeded, it should +        // not have had that chance. +        ensure_equals(get_test_name() + " script output", readfile(out.getName()), "ok"); +    } + +    template<> template<> +    void object::test<9>() +    { +        set_test_name("autokill=false"); +        NamedTempFile from("from", "not started"); +        NamedTempFile to("to", ""); +        LLProcess::handle phandle(0); +        { +            PythonProcessLauncher py(get_test_name(), +                                     "from __future__ import with_statement\n" +                                     "import sys, time\n" +                                     "with open(sys.argv[1], 'w') as f:\n" +                                     "    f.write('ok')\n" +                                     "# wait for 'go' from test program\n" +                                     "for i in xrange(60):\n" +                                     "    time.sleep(1)\n" +                                     "    with open(sys.argv[2]) as f:\n" +                                     "        go = f.read()\n" +                                     "    if go == 'go':\n" +                                     "        break\n" +                                     "else:\n" +                                     "    with open(sys.argv[1], 'w') as f:\n" +                                     "        f.write('never saw go')\n" +                                     "    sys.exit(1)\n" +                                     "# okay, saw 'go', write 'ack'\n" +                                     "with open(sys.argv[1], 'w') as f:\n" +                                     "    f.write('ack')\n"); +            py.mParams.args.add(from.getName()); +            py.mParams.args.add(to.getName()); +            py.mParams.autokill = false; +            py.launch(); +            // Capture handle for later +            phandle = py.mPy->getProcessHandle(); +            // Wait for the script to wake up and do its first write +            int i = 0, timeout = 60; +            for ( ; i < timeout; ++i) +            { +                yield(); +                if (readfile(from.getName(), "from autokill script") == "ok") +                    break; +            } +            // If we broke this loop because of the counter, something's wrong +            ensure("script never started", i < timeout); +            // Now destroy the LLProcess, which should NOT kill the child! +        } +        // If the destructor killed the child anyway, give it time to die +        yield(2); +        // How do we know it's not terminated? By making it respond to +        // a specific stimulus in a specific way. +        { +            std::ofstream outf(to.getName().c_str()); +            outf << "go"; +        } // flush and close. +        // now wait for the script to terminate... one way or another. +        waitfor(phandle, "autokill script"); +        // If the LLProcess destructor implicitly called kill(), the +        // script could not have written 'ack' as we expect. +        ensure_equals(get_test_name() + " script output", readfile(from.getName()), "ack"); +    } + +    template<> template<> +    void object::test<10>() +    { +        set_test_name("'bogus' test"); +        CaptureLog recorder; +        PythonProcessLauncher py(get_test_name(), +                                 "print 'Hello world'\n"); +        py.mParams.files.add(LLProcess::FileParam("bogus")); +        py.mPy = LLProcess::create(py.mParams); +        ensure("should have rejected 'bogus'", ! py.mPy); +        std::string message(recorder.messageWith("bogus")); +        ensure_contains("did not name 'stdin'", message, "stdin"); +    } + +    template<> template<> +    void object::test<11>() +    { +        set_test_name("'file' test"); +        // Replace this test with one or more real 'file' tests when we +        // implement 'file' support +        PythonProcessLauncher py(get_test_name(), +                                 "print 'Hello world'\n"); +        py.mParams.files.add(LLProcess::FileParam()); +        py.mParams.files.add(LLProcess::FileParam("file")); +        py.mPy = LLProcess::create(py.mParams); +        ensure("should have rejected 'file'", ! py.mPy); +    } + +    template<> template<> +    void object::test<12>() +    { +        set_test_name("'tpipe' test"); +        // Replace this test with one or more real 'tpipe' tests when we +        // implement 'tpipe' support +        CaptureLog recorder; +        PythonProcessLauncher py(get_test_name(), +                                 "print 'Hello world'\n"); +        py.mParams.files.add(LLProcess::FileParam()); +        py.mParams.files.add(LLProcess::FileParam("tpipe")); +        py.mPy = LLProcess::create(py.mParams); +        ensure("should have rejected 'tpipe'", ! py.mPy); +        std::string message(recorder.messageWith("tpipe")); +        ensure_contains("did not name 'stdout'", message, "stdout"); +    } + +    template<> template<> +    void object::test<13>() +    { +        set_test_name("'npipe' test"); +        // Replace this test with one or more real 'npipe' tests when we +        // implement 'npipe' support +        CaptureLog recorder; +        PythonProcessLauncher py(get_test_name(), +                                 "print 'Hello world'\n"); +        py.mParams.files.add(LLProcess::FileParam()); +        py.mParams.files.add(LLProcess::FileParam()); +        py.mParams.files.add(LLProcess::FileParam("npipe")); +        py.mPy = LLProcess::create(py.mParams); +        ensure("should have rejected 'npipe'", ! py.mPy); +        std::string message(recorder.messageWith("npipe")); +        ensure_contains("did not name 'stderr'", message, "stderr"); +    } + +    template<> template<> +    void object::test<14>() +    { +        set_test_name("internal pipe name warning"); +        CaptureLog recorder; +        PythonProcessLauncher py(get_test_name(), +                                 "import sys\n" +                                 "sys.exit(7)\n"); +        py.mParams.files.add(LLProcess::FileParam("pipe", "somename")); +        py.run();                   // verify that it did launch anyway +        ensure_equals("Status.mState", py.mPy->getStatus().mState, LLProcess::EXITED); +        ensure_equals("Status.mData",  py.mPy->getStatus().mData,  7); +        std::string message(recorder.messageWith("not yet supported")); +        ensure_contains("log message did not mention internal pipe name", +                        message, "somename"); +    } + +    /*-------------- support for "get*Pipe() validation" test --------------*/ +#define TEST_getPipe(PROCESS, GETPIPE, GETOPTPIPE, VALID, NOPIPE, BADPIPE) \ +    do                                                                  \ +    {                                                                   \ +        std::string threw;                                              \ +        /* Both the following calls should work. */                     \ +        (PROCESS).GETPIPE(VALID);                                       \ +        ensure(#GETOPTPIPE "(" #VALID ") failed", (PROCESS).GETOPTPIPE(VALID)); \ +        /* pass obviously bogus PIPESLOT */                             \ +        CATCH_IN(threw, LLProcess::NoPipe, (PROCESS).GETPIPE(LLProcess::FILESLOT(4))); \ +        ensure_contains("didn't reject bad slot", threw, "no slot");    \ +        ensure_contains("didn't mention bad slot num", threw, "4");     \ +        EXPECT_FAIL_WITH_LOG(threw, (PROCESS).GETOPTPIPE(LLProcess::FILESLOT(4))); \ +        /* pass NOPIPE */                                               \ +        CATCH_IN(threw, LLProcess::NoPipe, (PROCESS).GETPIPE(NOPIPE));  \ +        ensure_contains("didn't reject non-pipe", threw, "not a monitored"); \ +        EXPECT_FAIL_WITH_LOG(threw, (PROCESS).GETOPTPIPE(NOPIPE));      \ +        /* pass BADPIPE: FILESLOT isn't empty but wrong direction */    \ +        CATCH_IN(threw, LLProcess::NoPipe, (PROCESS).GETPIPE(BADPIPE)); \ +        /* sneaky: GETPIPE is getReadPipe or getWritePipe */            \ +        /* so skip "get" to obtain ReadPipe or WritePipe  :-P  */       \ +        ensure_contains("didn't reject wrong pipe", threw, (#GETPIPE)+3); \ +        EXPECT_FAIL_WITH_LOG(threw, (PROCESS).GETOPTPIPE(BADPIPE));     \ +    } while (0) + +/// For expecting exceptions. Execute CODE, catch EXCEPTION, store its what() +/// in std::string THREW, ensure it's not empty (i.e. EXCEPTION did happen). +#define CATCH_IN(THREW, EXCEPTION, CODE)                                \ +    do                                                                  \ +    {                                                                   \ +        (THREW).clear();                                                \ +        try                                                             \ +        {                                                               \ +            CODE;                                                       \ +        }                                                               \ +        CATCH_AND_STORE_WHAT_IN(THREW, EXCEPTION)                       \ +        ensure("failed to throw " #EXCEPTION ": " #CODE, ! (THREW).empty()); \ +    } while (0) + +#define EXPECT_FAIL_WITH_LOG(EXPECT, CODE)                              \ +    do                                                                  \ +    {                                                                   \ +        CaptureLog recorder;                                            \ +        ensure(#CODE " succeeded", ! (CODE));                           \ +        recorder.messageWith(EXPECT);                                   \ +    } while (0) + +    template<> template<> +    void object::test<15>() +    { +        set_test_name("get*Pipe() validation"); +        PythonProcessLauncher py(get_test_name(), +                                 "print 'this output is expected'\n"); +        py.mParams.files.add(LLProcess::FileParam("pipe")); // pipe for  stdin +        py.mParams.files.add(LLProcess::FileParam());       // inherit stdout +        py.mParams.files.add(LLProcess::FileParam("pipe")); // pipe for stderr +        py.run(); +        TEST_getPipe(*py.mPy, getWritePipe, getOptWritePipe, +                     LLProcess::STDIN,   // VALID +                     LLProcess::STDOUT,  // NOPIPE +                     LLProcess::STDERR); // BADPIPE +        TEST_getPipe(*py.mPy, getReadPipe,  getOptReadPipe, +                     LLProcess::STDERR,  // VALID +                     LLProcess::STDOUT,  // NOPIPE +                     LLProcess::STDIN);  // BADPIPE +    } + +    template<> template<> +    void object::test<16>() +    { +        set_test_name("talk to stdin/stdout"); +        PythonProcessLauncher py(get_test_name(), +                                 "import sys, time\n" +                                 "print 'ok'\n" +                                 "sys.stdout.flush()\n" +                                 "# wait for 'go' from test program\n" +                                 "go = sys.stdin.readline()\n" +                                 "if go != 'go\\n':\n" +                                 "    sys.exit('expected \"go\", saw %r' % go)\n" +                                 "print 'ack'\n"); +        py.mParams.files.add(LLProcess::FileParam("pipe")); // stdin +        py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout +        py.launch(); +        LLProcess::ReadPipe& childout(py.mPy->getReadPipe(LLProcess::STDOUT)); +        int i, timeout = 60; +        for (i = 0; i < timeout && py.mPy->isRunning() && childout.size() < 3; ++i) +        { +            yield(); +        } +        ensure("script never started", i < timeout); +        ensure_equals("bad wakeup from stdin/stdout script", +                      childout.getline(), "ok"); +        // important to get the implicit flush from std::endl +        py.mPy->getWritePipe().get_ostream() << "go" << std::endl; +        for (i = 0; i < timeout && py.mPy->isRunning() && ! childout.contains("\n"); ++i) +        { +            yield(); +        } +        ensure("script never replied", childout.contains("\n")); +        ensure_equals("child didn't ack", childout.getline(), "ack"); +        ensure_equals("bad child termination", py.mPy->getStatus().mState, LLProcess::EXITED); +        ensure_equals("bad child exit code",   py.mPy->getStatus().mData,  0); +    } + +    struct EventListener: public boost::noncopyable +    { +        EventListener(LLEventPump& pump) +        { +            mConnection =  +                pump.listen("EventListener", boost::bind(&EventListener::tick, this, _1)); +        } + +        bool tick(const LLSD& data) +        { +            mHistory.push_back(data); +            return false; +        } + +        std::list<LLSD> mHistory; +        LLTempBoundListener mConnection; +    }; + +    static bool ack(std::ostream& out, const LLSD& data) +    { +        out << "continue" << std::endl; +        return false; +    } + +    template<> template<> +    void object::test<17>() +    { +        set_test_name("listen for ReadPipe events"); +        PythonProcessLauncher py(get_test_name(), +                                 "import sys\n" +                                 "sys.stdout.write('abc')\n" +                                 "sys.stdout.flush()\n" +                                 "sys.stdin.readline()\n" +                                 "sys.stdout.write('def')\n" +                                 "sys.stdout.flush()\n" +                                 "sys.stdin.readline()\n" +                                 "sys.stdout.write('ghi\\n')\n" +                                 "sys.stdout.flush()\n" +                                 "sys.stdin.readline()\n" +                                 "sys.stdout.write('second line\\n')\n"); +        py.mParams.files.add(LLProcess::FileParam("pipe")); // stdin +        py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout +        py.launch(); +        std::ostream& childin(py.mPy->getWritePipe(LLProcess::STDIN).get_ostream()); +        LLProcess::ReadPipe& childout(py.mPy->getReadPipe(LLProcess::STDOUT)); +        // lift the default limit; allow event to carry (some of) the actual data +        childout.setLimit(20); +        // listen for incoming data on childout +        EventListener listener(childout.getPump()); +        // also listen with a function that prompts the child to continue +        // every time we see output +        LLTempBoundListener connection( +            childout.getPump().listen("ack", boost::bind(ack, boost::ref(childin), _1))); +        int i, timeout = 60; +        // wait through stuttering first line +        for (i = 0; i < timeout && py.mPy->isRunning() && ! childout.contains("\n"); ++i) +        { +            yield(); +        } +        ensure("couldn't get first line", i < timeout); +        // disconnect from listener +        listener.mConnection.disconnect(); +        // finish out the run +        waitfor(*py.mPy); +        // now verify history +        std::list<LLSD>::const_iterator li(listener.mHistory.begin()), +                                        lend(listener.mHistory.end()); +        ensure("no events", li != lend); +        ensure_equals("history[0]", (*li)["data"].asString(), "abc"); +        ensure_equals("history[0] len", (*li)["len"].asInteger(), 3); +        ++li; +        ensure("only 1 event", li != lend); +        ensure_equals("history[1]", (*li)["data"].asString(), "abcdef"); +        ensure_equals("history[0] len", (*li)["len"].asInteger(), 6); +        ++li; +        ensure("only 2 events", li != lend); +        ensure_equals("history[2]", (*li)["data"].asString(), "abcdefghi" EOL); +        ensure_equals("history[0] len", (*li)["len"].asInteger(), 9 + sizeof(EOL) - 1); +        ++li; +        // We DO NOT expect a whole new event for the second line because we +        // disconnected. +        ensure("more than 3 events", li == lend); +    } + +    template<> template<> +    void object::test<18>() +    { +        set_test_name("ReadPipe \"eof\" event"); +        PythonProcessLauncher py(get_test_name(), +                                 "print 'Hello from Python!'\n"); +        py.mParams.files.add(LLProcess::FileParam()); // stdin +        py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout +        py.launch(); +        LLProcess::ReadPipe& childout(py.mPy->getReadPipe(LLProcess::STDOUT)); +        EventListener listener(childout.getPump()); +        waitfor(*py.mPy); +        // We can't be positive there will only be a single event, if the OS +        // (or any other intervening layer) does crazy buffering. What we want +        // to ensure is that there was exactly ONE event with "eof" true, and +        // that it was the LAST event. +        std::list<LLSD>::const_reverse_iterator rli(listener.mHistory.rbegin()), +                                                rlend(listener.mHistory.rend()); +        ensure("no events", rli != rlend); +        ensure("last event not \"eof\"", (*rli)["eof"].asBoolean()); +        while (++rli != rlend) +        { +            ensure("\"eof\" event not last", ! (*rli)["eof"].asBoolean()); +        } +    } + +    template<> template<> +    void object::test<19>() +    { +        set_test_name("setLimit()"); +        PythonProcessLauncher py(get_test_name(), +                                 "import sys\n" +                                 "sys.stdout.write(sys.argv[1])\n"); +        std::string abc("abcdefghijklmnopqrstuvwxyz"); +        py.mParams.args.add(abc); +        py.mParams.files.add(LLProcess::FileParam()); // stdin +        py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout +        py.launch(); +        LLProcess::ReadPipe& childout(py.mPy->getReadPipe(LLProcess::STDOUT)); +        // listen for incoming data on childout +        EventListener listener(childout.getPump()); +        // but set limit +        childout.setLimit(10); +        ensure_equals("getLimit() after setlimit(10)", childout.getLimit(), 10); +        // okay, pump I/O to pick up output from child +        waitfor(*py.mPy); +        ensure("no events", ! listener.mHistory.empty()); +        // For all we know, that data could have arrived in several different +        // bursts... probably not, but anyway, only check the last one. +        ensure_equals("event[\"len\"]", +                      listener.mHistory.back()["len"].asInteger(), abc.length()); +        ensure_equals("length of setLimit(10) data", +                      listener.mHistory.back()["data"].asString().length(), 10); +    } + +    template<> template<> +    void object::test<20>() +    { +        set_test_name("peek() ReadPipe data"); +        PythonProcessLauncher py(get_test_name(), +                                 "import sys\n" +                                 "sys.stdout.write(sys.argv[1])\n"); +        std::string abc("abcdefghijklmnopqrstuvwxyz"); +        py.mParams.args.add(abc); +        py.mParams.files.add(LLProcess::FileParam()); // stdin +        py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout +        py.launch(); +        LLProcess::ReadPipe& childout(py.mPy->getReadPipe(LLProcess::STDOUT)); +        // okay, pump I/O to pick up output from child +        waitfor(*py.mPy); +        // peek() with substr args +        ensure_equals("peek()", childout.peek(), abc); +        ensure_equals("peek(23)", childout.peek(23), abc.substr(23)); +        ensure_equals("peek(5, 3)", childout.peek(5, 3), abc.substr(5, 3)); +        ensure_equals("peek(27, 2)", childout.peek(27, 2), ""); +        ensure_equals("peek(23, 5)", childout.peek(23, 5), "xyz"); +        // contains() -- we don't exercise as thoroughly as find() because the +        // contains() implementation is trivially (and visibly) based on find() +        ensure("contains(\":\")", ! childout.contains(":")); +        ensure("contains(':')",   ! childout.contains(':')); +        ensure("contains(\"d\")", childout.contains("d")); +        ensure("contains('d')",   childout.contains('d')); +        ensure("contains(\"klm\")", childout.contains("klm")); +        ensure("contains(\"klx\")", ! childout.contains("klx")); +        // find() +        ensure("find(\":\")", childout.find(":") == LLProcess::ReadPipe::npos); +        ensure("find(':')",   childout.find(':') == LLProcess::ReadPipe::npos); +        ensure_equals("find(\"d\")", childout.find("d"), 3); +        ensure_equals("find('d')",   childout.find('d'), 3); +        ensure_equals("find(\"d\", 3)", childout.find("d", 3), 3); +        ensure_equals("find('d', 3)",   childout.find('d', 3), 3); +        ensure("find(\"d\", 4)", childout.find("d", 4) == LLProcess::ReadPipe::npos); +        ensure("find('d', 4)",   childout.find('d', 4) == LLProcess::ReadPipe::npos); +        // The case of offset == end and offset > end are different. In the +        // first case, we can form a valid (albeit empty) iterator range and +        // search that. In the second, guard logic in the implementation must +        // realize we can't form a valid iterator range. +        ensure("find(\"d\", 26)", childout.find("d", 26) == LLProcess::ReadPipe::npos); +        ensure("find('d', 26)",   childout.find('d', 26) == LLProcess::ReadPipe::npos); +        ensure("find(\"d\", 27)", childout.find("d", 27) == LLProcess::ReadPipe::npos); +        ensure("find('d', 27)",   childout.find('d', 27) == LLProcess::ReadPipe::npos); +        ensure_equals("find(\"ghi\")", childout.find("ghi"), 6); +        ensure_equals("find(\"ghi\", 6)", childout.find("ghi"), 6); +        ensure("find(\"ghi\", 7)", childout.find("ghi", 7) == LLProcess::ReadPipe::npos); +        ensure("find(\"ghi\", 26)", childout.find("ghi", 26) == LLProcess::ReadPipe::npos); +        ensure("find(\"ghi\", 27)", childout.find("ghi", 27) == LLProcess::ReadPipe::npos); +    } + +    template<> template<> +    void object::test<21>() +    { +        set_test_name("bad postend"); +        std::string pumpname("postend"); +        EventListener listener(LLEventPumps::instance().obtain(pumpname)); +        LLProcess::Params params; +        params.desc = get_test_name(); +        params.postend = pumpname; +        LLProcessPtr child = LLProcess::create(params); +        ensure("shouldn't have launched", ! child); +        ensure_equals("number of postend events", listener.mHistory.size(), 1); +        LLSD postend(listener.mHistory.front()); +        ensure("has id", ! postend.has("id")); +        ensure_equals("desc", postend["desc"].asString(), std::string(params.desc)); +        ensure_equals("state", postend["state"].asInteger(), LLProcess::UNSTARTED); +        ensure("has data", ! postend.has("data")); +        std::string error(postend["string"]); +        // All we get from canned parameter validation is a bool, so the +        // "validation failed" message we ourselves generate can't mention +        // "executable" by name. Just check that it's nonempty. +        //ensure_contains("error", error, "executable"); +        ensure("string", ! error.empty()); +    } + +    template<> template<> +    void object::test<22>() +    { +        set_test_name("good postend"); +        PythonProcessLauncher py(get_test_name(), +                                 "import sys\n" +                                 "sys.exit(35)\n"); +        std::string pumpname("postend"); +        EventListener listener(LLEventPumps::instance().obtain(pumpname)); +        py.mParams.postend = pumpname; +        py.launch(); +        LLProcess::id childid(py.mPy->getProcessID()); +        // Don't use waitfor(), which calls isRunning(); instead wait for an +        // event on pumpname. +        int i, timeout = 60; +        for (i = 0; i < timeout && listener.mHistory.empty(); ++i) +        { +            yield(); +        } +        ensure("no postend event", i < timeout); +        ensure_equals("number of postend events", listener.mHistory.size(), 1); +        LLSD postend(listener.mHistory.front()); +        ensure_equals("id",    postend["id"].asInteger(), childid); +        ensure("desc empty", ! postend["desc"].asString().empty()); +        ensure_equals("state", postend["state"].asInteger(), LLProcess::EXITED); +        ensure_equals("data",  postend["data"].asInteger(),  35); +        std::string str(postend["string"]); +        ensure_contains("string", str, "exited"); +        ensure_contains("string", str, "35"); +    } + +    struct PostendListener +    { +        PostendListener(LLProcess::ReadPipe& rpipe, +                        const std::string& pumpname, +                        const std::string& expect): +            mReadPipe(rpipe), +            mExpect(expect), +            mTriggered(false) +        { +            LLEventPumps::instance().obtain(pumpname) +                .listen("PostendListener", boost::bind(&PostendListener::postend, this, _1)); +        } + +        bool postend(const LLSD&) +        { +            mTriggered = true; +            ensure_equals("postend listener", mReadPipe.read(mReadPipe.size()), mExpect); +            return false; +        } + +        LLProcess::ReadPipe& mReadPipe; +        std::string mExpect; +        bool mTriggered; +    }; + +    template<> template<> +    void object::test<23>() +    { +        set_test_name("all data visible at postend"); +        PythonProcessLauncher py(get_test_name(), +                                 "import sys\n" +                                 // note, no '\n' in written data +                                 "sys.stdout.write('partial line')\n"); +        std::string pumpname("postend"); +        py.mParams.files.add(LLProcess::FileParam()); // stdin +        py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout +        py.mParams.postend = pumpname; +        py.launch(); +        PostendListener listener(py.mPy->getReadPipe(LLProcess::STDOUT), +                                 pumpname, +                                 "partial line"); +        waitfor(*py.mPy); +        ensure("postend never triggered", listener.mTriggered); +    } +} // namespace tut diff --git a/indra/llcommon/tests/llsdserialize_test.cpp b/indra/llcommon/tests/llsdserialize_test.cpp index 72322c3b72..e625545763 100644 --- a/indra/llcommon/tests/llsdserialize_test.cpp +++ b/indra/llcommon/tests/llsdserialize_test.cpp @@ -40,41 +40,15 @@ typedef U32 uint32_t;  #include <fcntl.h>  #include <sys/stat.h>  #include <sys/wait.h> -#include "llprocesslauncher.h" +#include "llprocess.h"  #endif -#include <sstream> - -/*==========================================================================*| -// Whoops, seems Linden's Boost package and the viewer are built with -// different settings of VC's /Zc:wchar_t switch! Using Boost.Filesystem -// pathname operations produces Windows link errors: -// unresolved external symbol "private: static class std::codecvt<unsigned short, -// char,int> const * & __cdecl boost::filesystem3::path::wchar_t_codecvt_facet()" -// unresolved external symbol "void __cdecl boost::filesystem3::path_traits::convert()" -// See: -// http://boost.2283326.n4.nabble.com/filesystem-v3-unicode-and-std-codecvt-linker-error-td3455549.html -// which points to: -// http://msdn.microsoft.com/en-us/library/dh8che7s%28v=VS.100%29.aspx - -// As we're not trying to preserve compatibility with old Boost.Filesystem -// code, but rather writing brand-new code, use the newest available -// Filesystem API. -#define BOOST_FILESYSTEM_VERSION 3 -#include "boost/filesystem.hpp" -#include "boost/filesystem/v3/fstream.hpp" -|*==========================================================================*/  #include "boost/range.hpp"  #include "boost/foreach.hpp"  #include "boost/function.hpp"  #include "boost/lambda/lambda.hpp"  #include "boost/lambda/bind.hpp"  namespace lambda = boost::lambda; -/*==========================================================================*| -// Aaaarrgh, Linden's Boost package doesn't even include Boost.Iostreams! -#include "boost/iostreams/stream.hpp" -#include "boost/iostreams/device/file_descriptor.hpp" -|*==========================================================================*/  #include "../llsd.h"  #include "../llsdserialize.h" @@ -82,236 +56,17 @@ namespace lambda = boost::lambda;  #include "../llformat.h"  #include "../test/lltut.h" +#include "../test/manageapr.h" +#include "../test/namedtempfile.h"  #include "stringize.h" +static ManageAPR manager; +  std::vector<U8> string_to_vector(const std::string& str)  {  	return std::vector<U8>(str.begin(), str.end());  } -#if ! LL_WINDOWS -// We want to call strerror_r(), but alarmingly, there are two different -// variants. The one that returns int always populates the passed buffer -// (except in case of error), whereas the other one always returns a valid -// char* but might or might not populate the passed buffer. How do we know -// which one we're getting? Define adapters for each and let the compiler -// select the applicable adapter. - -// strerror_r() returns char* -std::string message_from(int /*orig_errno*/, const char* /*buffer*/, const char* strerror_ret) -{ -    return strerror_ret; -} - -// strerror_r() returns int -std::string message_from(int orig_errno, const char* buffer, int strerror_ret) -{ -    if (strerror_ret == 0) -    { -        return buffer; -    } -    // Here strerror_r() has set errno. Since strerror_r() has already failed, -    // seems like a poor bet to call it again to diagnose its own error... -    int stre_errno = errno; -    if (stre_errno == ERANGE) -    { -        return STRINGIZE("strerror_r() can't explain errno " << orig_errno -                         << " (buffer too small)"); -    } -    if (stre_errno == EINVAL) -    { -        return STRINGIZE("unknown errno " << orig_errno); -    } -    // Here we don't even understand the errno from strerror_r()! -    return STRINGIZE("strerror_r() can't explain errno " << orig_errno -                     << " (error " << stre_errno << ')'); -} -#endif  // ! LL_WINDOWS - -// boost::filesystem::temp_directory_path() isn't yet in Boost 1.45! :-( -std::string temp_directory_path() -{ -#if LL_WINDOWS -    char buffer[4096]; -    GetTempPathA(sizeof(buffer), buffer); -    return buffer; - -#else  // LL_DARWIN, LL_LINUX -    static const char* vars[] = { "TMPDIR", "TMP", "TEMP", "TEMPDIR" }; -    BOOST_FOREACH(const char* var, vars) -    { -        const char* found = getenv(var); -        if (found) -            return found; -    } -    return "/tmp"; -#endif // LL_DARWIN, LL_LINUX -} - -// Windows presents a kinda sorta compatibility layer. Code to the yucky -// Windows names because they're less likely than the Posix names to collide -// with any other names in this source. -#if LL_WINDOWS -#define _remove   DeleteFileA -#else  // ! LL_WINDOWS -#define _open     open -#define _write    write -#define _close    close -#define _remove   remove -#endif  // ! LL_WINDOWS - -// Create a text file with specified content "somewhere in the -// filesystem," cleaning up when it goes out of scope. -class NamedTempFile -{ -public: -    // Function that accepts an ostream ref and (presumably) writes stuff to -    // it, e.g.: -    // (lambda::_1 << "the value is " << 17 << '\n') -    typedef boost::function<void(std::ostream&)> Streamer; - -    NamedTempFile(const std::string& ext, const std::string& content): -        mPath(temp_directory_path()) -    { -        createFile(ext, lambda::_1 << content); -    } - -    // Disambiguate when passing string literal -    NamedTempFile(const std::string& ext, const char* content): -        mPath(temp_directory_path()) -    { -        createFile(ext, lambda::_1 << content); -    } - -    NamedTempFile(const std::string& ext, const Streamer& func): -        mPath(temp_directory_path()) -    { -        createFile(ext, func); -    } - -    ~NamedTempFile() -    { -        _remove(mPath.c_str()); -    } - -    std::string getName() const { return mPath; } - -private: -    void createFile(const std::string& ext, const Streamer& func) -    { -        // Silly maybe, but use 'ext' as the name prefix. Strip off a leading -        // '.' if present. -        int pfx_offset = ((! ext.empty()) && ext[0] == '.')? 1 : 0; - -#if ! LL_WINDOWS -        // Make sure mPath ends with a directory separator, if it doesn't already. -        if (mPath.empty() || -            ! (mPath[mPath.length() - 1] == '\\' || mPath[mPath.length() - 1] == '/')) -        { -            mPath.append("/"); -        } - -        // mkstemp() accepts and modifies a char* template string. Generate -        // the template string, then copy to modifiable storage. -        // mkstemp() requires its template string to end in six X's. -        mPath += ext.substr(pfx_offset) + "XXXXXX"; -        // Copy to vector<char> -        std::vector<char> pathtemplate(mPath.begin(), mPath.end()); -        // append a nul byte for classic-C semantics -        pathtemplate.push_back('\0'); -        // std::vector promises that a pointer to the 0th element is the same -        // as a pointer to a contiguous classic-C array -        int fd(mkstemp(&pathtemplate[0])); -        if (fd == -1) -        { -            // The documented errno values (http://linux.die.net/man/3/mkstemp) -            // are used in a somewhat unusual way, so provide context-specific -            // errors. -            if (errno == EEXIST) -            { -                LL_ERRS("NamedTempFile") << "mkstemp(\"" << mPath -                                         << "\") could not create unique file " << LL_ENDL; -            } -            if (errno == EINVAL) -            { -                LL_ERRS("NamedTempFile") << "bad mkstemp() file path template '" -                                         << mPath << "'" << LL_ENDL; -            } -            // Shrug, something else -            int mkst_errno = errno; -            char buffer[256]; -            LL_ERRS("NamedTempFile") << "mkstemp(\"" << mPath << "\") failed: " -                                     << message_from(mkst_errno, buffer, -                                                     strerror_r(mkst_errno, buffer, sizeof(buffer))) -                                     << LL_ENDL; -        } -        // mkstemp() seems to have worked! Capture the modified filename. -        // Avoid the nul byte we appended. -        mPath.assign(pathtemplate.begin(), (pathtemplate.end()-1)); - -/*==========================================================================*| -        // Define an ostream on the open fd. Tell it to close fd on destruction. -        boost::iostreams::stream<boost::iostreams::file_descriptor_sink> -            out(fd, boost::iostreams::close_handle); -|*==========================================================================*/ - -        // Write desired content. -        std::ostringstream out; -        // Stream stuff to it. -        func(out); - -        std::string data(out.str()); -        int written(_write(fd, data.c_str(), data.length())); -        int closed(_close(fd)); -        llassert_always(written == data.length() && closed == 0); - -#else // LL_WINDOWS -        // GetTempFileName() is documented to require a MAX_PATH buffer. -        char tempname[MAX_PATH]; -        // Use 'ext' as filename prefix, but skip leading '.' if any. -        // The 0 param is very important: requests iterating until we get a -        // unique name. -        if (0 == GetTempFileNameA(mPath.c_str(), ext.c_str() + pfx_offset, 0, tempname)) -        { -            // I always have to look up this call...  :-P -            LPSTR msgptr; -            FormatMessageA( -                FORMAT_MESSAGE_ALLOCATE_BUFFER |  -                FORMAT_MESSAGE_FROM_SYSTEM | -                FORMAT_MESSAGE_IGNORE_INSERTS, -                NULL, -                GetLastError(), -                MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), -                LPSTR(&msgptr),     // have to cast (char**) to (char*) -                0, NULL ); -            LL_ERRS("NamedTempFile") << "GetTempFileName(\"" << mPath << "\", \"" -                                     << (ext.c_str() + pfx_offset) << "\") failed: " -                                     << msgptr << LL_ENDL; -            LocalFree(msgptr); -        } -        // GetTempFileName() appears to have worked! Capture the actual -        // filename. -        mPath = tempname; -        // Open the file and stream content to it. Destructor will close. -        std::ofstream out(tempname); -        func(out); - -#endif  // LL_WINDOWS -    } - -    void peep() -    { -        std::cout << "File '" << mPath << "' contains:\n"; -        std::ifstream reader(mPath.c_str()); -        std::string line; -        while (std::getline(reader, line)) -            std::cout << line << '\n'; -        std::cout << "---\n"; -    } - -    std::string mPath; -}; -  namespace tut  {  	struct sd_xml_data @@ -1783,7 +1538,7 @@ namespace tut              const char* PYTHON(getenv("PYTHON"));              ensure("Set $PYTHON to the Python interpreter", PYTHON); -            NamedTempFile scriptfile(".py", script); +            NamedTempFile scriptfile("py", script);  #if LL_WINDOWS              std::string q("\""); @@ -1802,14 +1557,15 @@ namespace tut              }  #else  // LL_DARWIN, LL_LINUX -            LLProcessLauncher py; -            py.setExecutable(PYTHON); -            py.addArgument(scriptfile.getName()); -            ensure_equals(STRINGIZE("Couldn't launch " << desc << " script"), py.launch(), 0); +            LLProcess::Params params; +            params.executable = PYTHON; +            params.args.add(scriptfile.getName()); +            LLProcessPtr py(LLProcess::create(params)); +            ensure(STRINGIZE("Couldn't launch " << desc << " script"), py);              // Implementing timeout would mean messing with alarm() and              // catching SIGALRM... later maybe...              int status(0); -            if (waitpid(py.getProcessID(), &status, 0) == -1) +            if (waitpid(py->getProcessID(), &status, 0) == -1)              {                  int waitpid_errno(errno);                  ensure_equals(STRINGIZE("Couldn't retrieve rc from " << desc << " script: " @@ -1888,12 +1644,12 @@ namespace tut              "    else:\n"              "        assert False, 'Too many data items'\n"; -        // Create a something.llsd file containing 'data' serialized to +        // Create an llsdXXXXXX file containing 'data' serialized to          // notation. It's important to separate with newlines because Python's          // llsd module doesn't support parsing from a file stream, only from a          // string, so we have to know how much of the file to read into a          // string. -        NamedTempFile file(".llsd", +        NamedTempFile file("llsd",                             // NamedTempFile's boost::function constructor                             // takes a callable. To this callable it passes the                             // std::ostream with which it's writing the @@ -1926,7 +1682,7 @@ namespace tut          // Create an empty data file. This is just a placeholder for our          // script to write into. Create it to establish a unique name that          // we know. -        NamedTempFile file(".llsd", ""); +        NamedTempFile file("llsd", "");          python("write Python notation",                 lambda::_1 << diff --git a/indra/llcommon/tests/llstreamqueue_test.cpp b/indra/llcommon/tests/llstreamqueue_test.cpp new file mode 100644 index 0000000000..050ad5c5bf --- /dev/null +++ b/indra/llcommon/tests/llstreamqueue_test.cpp @@ -0,0 +1,197 @@ +/** + * @file   llstreamqueue_test.cpp + * @author Nat Goodspeed + * @date   2012-01-05 + * @brief  Test for llstreamqueue. + *  + * $LicenseInfo:firstyear=2012&license=viewerlgpl$ + * Copyright (c) 2012, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "llstreamqueue.h" +// STL headers +#include <vector> +// std headers +// external library headers +#include <boost/foreach.hpp> +// other Linden headers +#include "../test/lltut.h" +#include "stringize.h" + +/***************************************************************************** +*   TUT +*****************************************************************************/ +namespace tut +{ +    struct llstreamqueue_data +    { +        llstreamqueue_data(): +            // we want a buffer with actual bytes in it, not an empty vector +            buffer(10) +        {} +        // As LLStreamQueue is merely a typedef for +        // LLGenericStreamQueue<char>, and no logic in LLGenericStreamQueue is +        // specific to the <char> instantiation, we're comfortable for now +        // testing only the narrow-char version. +        LLStreamQueue strq; +        // buffer for use in multiple tests +        std::vector<char> buffer; +    }; +    typedef test_group<llstreamqueue_data> llstreamqueue_group; +    typedef llstreamqueue_group::object object; +    llstreamqueue_group llstreamqueuegrp("llstreamqueue"); + +    template<> template<> +    void object::test<1>() +    { +        set_test_name("empty LLStreamQueue"); +        ensure_equals("brand-new LLStreamQueue isn't empty", +                      strq.size(), 0); +        ensure_equals("brand-new LLStreamQueue returns data", +                      strq.asSource().read(&buffer[0], buffer.size()), 0); +        strq.asSink().close(); +        ensure_equals("closed empty LLStreamQueue not at EOF", +                      strq.asSource().read(&buffer[0], buffer.size()), -1); +    } + +    template<> template<> +    void object::test<2>() +    { +        set_test_name("one internal block, one buffer"); +        LLStreamQueue::Sink sink(strq.asSink()); +        ensure_equals("write(\"\")", sink.write("", 0), 0); +        ensure_equals("0 write should leave LLStreamQueue empty (size())", +                      strq.size(), 0); +        ensure_equals("0 write should leave LLStreamQueue empty (peek())", +                      strq.peek(&buffer[0], buffer.size()), 0); +        // The meaning of "atomic" is that it must be smaller than our buffer. +        std::string atomic("atomic"); +        ensure("test data exceeds buffer", atomic.length() < buffer.size()); +        ensure_equals(STRINGIZE("write(\"" << atomic << "\")"), +                      sink.write(&atomic[0], atomic.length()), atomic.length()); +        ensure_equals("size() after write()", strq.size(), atomic.length()); +        size_t peeklen(strq.peek(&buffer[0], buffer.size())); +        ensure_equals(STRINGIZE("peek(\"" << atomic << "\")"), +                      peeklen, atomic.length()); +        ensure_equals(STRINGIZE("peek(\"" << atomic << "\") result"), +                      std::string(buffer.begin(), buffer.begin() + peeklen), atomic); +        ensure_equals("size() after peek()", strq.size(), atomic.length()); +        // peek() should not consume. Use a different buffer to prove it isn't +        // just leftover data from the first peek(). +        std::vector<char> again(buffer.size()); +        peeklen = size_t(strq.peek(&again[0], again.size())); +        ensure_equals(STRINGIZE("peek(\"" << atomic << "\") again"), +                      peeklen, atomic.length()); +        ensure_equals(STRINGIZE("peek(\"" << atomic << "\") again result"), +                      std::string(again.begin(), again.begin() + peeklen), atomic); +        // now consume. +        std::vector<char> third(buffer.size()); +        size_t readlen(strq.read(&third[0], third.size())); +        ensure_equals(STRINGIZE("read(\"" << atomic << "\")"), +                      readlen, atomic.length()); +        ensure_equals(STRINGIZE("read(\"" << atomic << "\") result"), +                      std::string(third.begin(), third.begin() + readlen), atomic); +        ensure_equals("peek() after read()", strq.peek(&buffer[0], buffer.size()), 0); +        ensure_equals("size() after read()", strq.size(), 0); +    } + +    template<> template<> +    void object::test<3>() +    { +        set_test_name("basic skip()"); +        std::string lovecraft("lovecraft"); +        ensure("test data exceeds buffer", lovecraft.length() < buffer.size()); +        ensure_equals(STRINGIZE("write(\"" << lovecraft << "\")"), +                      strq.write(&lovecraft[0], lovecraft.length()), lovecraft.length()); +        size_t peeklen(strq.peek(&buffer[0], buffer.size())); +        ensure_equals(STRINGIZE("peek(\"" << lovecraft << "\")"), +                      peeklen, lovecraft.length()); +        ensure_equals(STRINGIZE("peek(\"" << lovecraft << "\") result"), +                      std::string(buffer.begin(), buffer.begin() + peeklen), lovecraft); +        std::streamsize skip1(4); +        ensure_equals(STRINGIZE("skip(" << skip1 << ")"), strq.skip(skip1), skip1); +        ensure_equals("size() after skip()", strq.size(), lovecraft.length() - skip1); +        size_t readlen(strq.read(&buffer[0], buffer.size())); +        ensure_equals(STRINGIZE("read(\"" << lovecraft.substr(skip1) << "\")"), +                      readlen, lovecraft.length() - skip1); +        ensure_equals(STRINGIZE("read(\"" << lovecraft.substr(skip1) << "\") result"), +                      std::string(buffer.begin(), buffer.begin() + readlen), +                      lovecraft.substr(skip1)); +        ensure_equals("unconsumed", strq.read(&buffer[0], buffer.size()), 0); +    } + +    template<> template<> +    void object::test<4>() +    { +        set_test_name("skip() multiple blocks"); +        std::string blocks[] = { "books of ", "H.P. ", "Lovecraft" }; +        std::streamsize total(blocks[0].length() + blocks[1].length() + blocks[2].length()); +        std::streamsize leave(5);   // len("craft") above +        std::streamsize skip(total - leave); +        std::streamsize written(0); +        BOOST_FOREACH(const std::string& block, blocks) +        { +            written += strq.write(&block[0], block.length()); +            ensure_equals("size() after write()", strq.size(), written); +        } +        std::streamsize skiplen(strq.skip(skip)); +        ensure_equals(STRINGIZE("skip(" << skip << ")"), skiplen, skip); +        ensure_equals("size() after skip()", strq.size(), leave); +        size_t readlen(strq.read(&buffer[0], buffer.size())); +        ensure_equals("read(\"craft\")", readlen, leave); +        ensure_equals("read(\"craft\") result", +                      std::string(buffer.begin(), buffer.begin() + readlen), "craft"); +    } + +    template<> template<> +    void object::test<5>() +    { +        set_test_name("concatenate blocks"); +        std::string blocks[] = { "abcd", "efghij", "klmnopqrs" }; +        BOOST_FOREACH(const std::string& block, blocks) +        { +            strq.write(&block[0], block.length()); +        } +        std::vector<char> longbuffer(30); +        std::streamsize readlen(strq.read(&longbuffer[0], longbuffer.size())); +        ensure_equals("read() multiple blocks", +                      readlen, blocks[0].length() + blocks[1].length() + blocks[2].length()); +        ensure_equals("read() multiple blocks result", +                      std::string(longbuffer.begin(), longbuffer.begin() + readlen), +                      blocks[0] + blocks[1] + blocks[2]); +    } + +    template<> template<> +    void object::test<6>() +    { +        set_test_name("split blocks"); +        std::string blocks[] = { "abcdefghijklm", "nopqrstuvwxyz" }; +        BOOST_FOREACH(const std::string& block, blocks) +        { +            strq.write(&block[0], block.length()); +        } +        strq.close(); +        // We've already verified what strq.size() should be at this point; +        // see above test named "skip() multiple blocks" +        std::streamsize chksize(strq.size()); +        std::streamsize readlen(strq.read(&buffer[0], buffer.size())); +        ensure_equals("read() 0", readlen, buffer.size()); +        ensure_equals("read() 0 result", std::string(buffer.begin(), buffer.end()), "abcdefghij"); +        chksize -= readlen; +        ensure_equals("size() after read() 0", strq.size(), chksize); +        readlen = strq.read(&buffer[0], buffer.size()); +        ensure_equals("read() 1", readlen, buffer.size()); +        ensure_equals("read() 1 result", std::string(buffer.begin(), buffer.end()), "klmnopqrst"); +        chksize -= readlen; +        ensure_equals("size() after read() 1", strq.size(), chksize); +        readlen = strq.read(&buffer[0], buffer.size()); +        ensure_equals("read() 2", readlen, chksize); +        ensure_equals("read() 2 result", +                      std::string(buffer.begin(), buffer.begin() + readlen), "uvwxyz"); +        ensure_equals("read() 3", strq.read(&buffer[0], buffer.size()), -1); +    } +} // namespace tut diff --git a/indra/llcommon/tests/llstring_test.cpp b/indra/llcommon/tests/llstring_test.cpp index 6a1cbf652a..93d3968dbf 100644 --- a/indra/llcommon/tests/llstring_test.cpp +++ b/indra/llcommon/tests/llstring_test.cpp @@ -29,7 +29,11 @@  #include "linden_common.h"  #include "../test/lltut.h" +#include <boost/assign/list_of.hpp>  #include "../llstring.h" +#include "StringVec.h" + +using boost::assign::list_of;  namespace tut  { @@ -750,4 +754,118 @@ namespace tut  		ensure("empty substr.", !LLStringUtil::endsWith(empty, value));  		ensure("empty everything.", !LLStringUtil::endsWith(empty, empty));  	} + +	template<> template<> +	void string_index_object_t::test<41>() +	{ +		set_test_name("getTokens(\"delims\")"); +		ensure_equals("empty string", LLStringUtil::getTokens("", " "), StringVec()); +		ensure_equals("only delims", +					  LLStringUtil::getTokens("   \r\n   ", " \r\n"), StringVec()); +		ensure_equals("sequence of delims", +					  LLStringUtil::getTokens(",,, one ,,,", ","), list_of("one")); +		// nat considers this a dubious implementation side effect, but I'd +		// hate to change it now... +		ensure_equals("noncontiguous tokens", +					  LLStringUtil::getTokens(", ,, , one ,,,", ","), list_of("")("")("one")); +		ensure_equals("space-padded tokens", +					  LLStringUtil::getTokens(",    one  ,  two  ,", ","), list_of("one")("two")); +		ensure_equals("no delims", LLStringUtil::getTokens("one", ","), list_of("one")); +	} + +	// Shorthand for verifying that getTokens() behaves the same when you +	// don't pass a string of escape characters, when you pass an empty string +	// (different overloads), and when you pass a string of characters that +	// aren't actually present. +	void ensure_getTokens(const std::string& desc, +						  const std::string& string, +						  const std::string& drop_delims, +						  const std::string& keep_delims, +						  const std::string& quotes, +						  const std::vector<std::string>& expect) +	{ +		ensure_equals(desc + " - no esc", +					  LLStringUtil::getTokens(string, drop_delims, keep_delims, quotes), +					  expect); +		ensure_equals(desc + " - empty esc", +					  LLStringUtil::getTokens(string, drop_delims, keep_delims, quotes, ""), +					  expect); +		ensure_equals(desc + " - unused esc", +					  LLStringUtil::getTokens(string, drop_delims, keep_delims, quotes, "!"), +					  expect); +	} + +	void ensure_getTokens(const std::string& desc, +						  const std::string& string, +						  const std::string& drop_delims, +						  const std::string& keep_delims, +						  const std::vector<std::string>& expect) +	{ +		ensure_getTokens(desc, string, drop_delims, keep_delims, "", expect); +	} + +	template<> template<> +	void string_index_object_t::test<42>() +	{ +		set_test_name("getTokens(\"delims\", etc.)"); +		// Signatures to test in this method: +		// getTokens(string, drop_delims, keep_delims [, quotes [, escapes]]) +		// If you omit keep_delims, you get the older function (test above). + +		// cases like the getTokens(string, delims) tests above +		ensure_getTokens("empty string", "", " ", "", StringVec()); +		ensure_getTokens("only delims", +						 "   \r\n   ", " \r\n", "", StringVec()); +		ensure_getTokens("sequence of delims", +						 ",,, one ,,,", ", ", "", list_of("one")); +		// Note contrast with the case in the previous method +		ensure_getTokens("noncontiguous tokens", +						 ", ,, , one ,,,", ", ", "", list_of("one")); +		ensure_getTokens("space-padded tokens", +						 ",    one  ,  two  ,", ", ", "", +                         list_of("one")("two")); +		ensure_getTokens("no delims", "one", ",", "", list_of("one")); + +		// drop_delims vs. keep_delims +		ensure_getTokens("arithmetic", +						 " ab+def  / xx*  yy ", " ", "+-*/", +						 list_of("ab")("+")("def")("/")("xx")("*")("yy")); + +		// quotes +		ensure_getTokens("no quotes", +						 "She said, \"Don't go.\"", " ", ",", "", +						 list_of("She")("said")(",")("\"Don't")("go.\"")); +		ensure_getTokens("quotes", +						 "She said, \"Don't go.\"", " ", ",", "\"", +						 list_of("She")("said")(",")("Don't go.")); +		ensure_getTokens("quotes and delims", +						 "run c:/'Documents and Settings'/someone", " ", "", "'", +						 list_of("run")("c:/Documents and Settings/someone")); +		ensure_getTokens("unmatched quote", +						 "baby don't leave", " ", "", "'", +						 list_of("baby")("don't")("leave")); +		ensure_getTokens("adjacent quoted", +						 "abc'def \"ghi'\"jkl' mno\"pqr", " ", "", "\"'", +						 list_of("abcdef \"ghijkl' mnopqr")); +		ensure_getTokens("quoted empty string", +						 "--set SomeVar ''", " ", "", "'", +						 list_of("--set")("SomeVar")("")); + +		// escapes +		// Don't use backslash as an escape for these tests -- you'll go nuts +		// between the C++ string scanner and getTokens() escapes. Test with +		// something else! +		ensure_equals("escaped delims", +					  LLStringUtil::getTokens("^ a - dog^-gone^ phrase", " ", "-", "", "^"), +					  list_of(" a")("-")("dog-gone phrase")); +		ensure_equals("escaped quotes", +					  LLStringUtil::getTokens("say: 'this isn^'t w^orking'.", " ", "", "'", "^"), +					  list_of("say:")("this isn't working.")); +		ensure_equals("escaped escape", +					  LLStringUtil::getTokens("want x^^2", " ", "", "", "^"), +					  list_of("want")("x^2")); +		ensure_equals("escape at end", +					  LLStringUtil::getTokens("it's^ up there^", " ", "", "'", "^"), +					  list_of("it's up")("there^")); +    }  } diff --git a/indra/llcommon/tests/setpython.py b/indra/llcommon/tests/setpython.py deleted file mode 100644 index df7b90428e..0000000000 --- a/indra/llcommon/tests/setpython.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/python -"""\ -@file   setpython.py -@author Nat Goodspeed -@date   2011-07-13 -@brief  Set PYTHON environment variable for tests that care. - -$LicenseInfo:firstyear=2011&license=viewerlgpl$ -Copyright (c) 2011, Linden Research, Inc. -$/LicenseInfo$ -""" - -import os -import sys -import subprocess - -if __name__ == "__main__": -    os.environ["PYTHON"] = sys.executable -    sys.exit(subprocess.call(sys.argv[1:])) diff --git a/indra/llcommon/tests/wrapllerrs.h b/indra/llcommon/tests/wrapllerrs.h index ffda84729b..a4d3a4e026 100644 --- a/indra/llcommon/tests/wrapllerrs.h +++ b/indra/llcommon/tests/wrapllerrs.h @@ -29,7 +29,22 @@  #if ! defined(LL_WRAPLLERRS_H)  #define LL_WRAPLLERRS_H +#if LL_WINDOWS +#pragma warning (disable : 4355) // 'this' used in initializer list: yes, intentionally +#endif + +#include <tut/tut.hpp>  #include "llerrorcontrol.h" +#include "stringize.h" +#include <boost/bind.hpp> +#include <boost/noncopyable.hpp> +#include <list> +#include <string> +#include <stdexcept> + +// statically reference the function in test.cpp... it's short, we could +// replicate, but better to reuse +extern void wouldHaveCrashed(const std::string& message);  struct WrapLL_ERRS  { @@ -70,4 +85,118 @@ struct WrapLL_ERRS      LLError::FatalFunction mPriorFatal;  }; +/** + * LLError::addRecorder() accepts ownership of the passed Recorder* -- it + * expects to be able to delete it later. CaptureLog isa Recorder whose + * pointer we want to be able to pass without any ownership implications. + * For such cases, instantiate a new RecorderProxy(yourRecorder) and pass + * that. Your heap RecorderProxy might later be deleted, but not yourRecorder. + */ +class RecorderProxy: public LLError::Recorder +{ +public: +    RecorderProxy(LLError::Recorder* recorder): +        mRecorder(recorder) +    {} + +    virtual void recordMessage(LLError::ELevel level, const std::string& message) +    { +        mRecorder->recordMessage(level, message); +    } + +    virtual bool wantsTime() +    { +        return mRecorder->wantsTime(); +    } + +private: +    LLError::Recorder* mRecorder; +}; + +/** + * Capture log messages. This is adapted (simplified) from the one in + * llerror_test.cpp. + */ +class CaptureLog : public LLError::Recorder, public boost::noncopyable +{ +public: +    CaptureLog(LLError::ELevel level=LLError::LEVEL_DEBUG): +        // Mostly what we're trying to accomplish by saving and resetting +        // LLError::Settings is to bypass the default RecordToStderr and +        // RecordToWinDebug Recorders. As these are visible only inside +        // llerror.cpp, we can't just call LLError::removeRecorder() with +        // each. For certain tests we need to produce, capture and examine +        // DEBUG log messages -- but we don't want to spam the user's console +        // with that output. If it turns out that saveAndResetSettings() has +        // some bad effect, give up and just let the DEBUG level log messages +        // display. +        mOldSettings(LLError::saveAndResetSettings()), +        mProxy(new RecorderProxy(this)) +    { +        LLError::setFatalFunction(wouldHaveCrashed); +        LLError::setDefaultLevel(level); +        LLError::addRecorder(mProxy); +    } + +    ~CaptureLog() +    { +        LLError::removeRecorder(mProxy); +        delete mProxy; +        LLError::restoreSettings(mOldSettings); +    } + +    void recordMessage(LLError::ELevel level, +                       const std::string& message) +    { +        mMessages.push_back(message); +    } + +    /// Don't assume the message we want is necessarily the LAST log message +    /// emitted by the underlying code; search backwards through all messages +    /// for the sought string. +    std::string messageWith(const std::string& search, bool required=true) +    { +        for (MessageList::const_reverse_iterator rmi(mMessages.rbegin()), rmend(mMessages.rend()); +             rmi != rmend; ++rmi) +        { +            if (rmi->find(search) != std::string::npos) +                return *rmi; +        } +        // failed to find any such message +        if (! required) +            return std::string(); + +        throw tut::failure(STRINGIZE("failed to find '" << search +                                     << "' in captured log messages:\n" +                                     << boost::ref(*this))); +    } + +    std::ostream& streamto(std::ostream& out) const +    { +        MessageList::const_iterator mi(mMessages.begin()), mend(mMessages.end()); +        if (mi != mend) +        { +            // handle first message separately: it doesn't get a newline +            out << *mi++; +            for ( ; mi != mend; ++mi) +            { +                // every subsequent message gets a newline +                out << '\n' << *mi; +            } +        } +        return out; +    } + +    typedef std::list<std::string> MessageList; +    MessageList mMessages; +    LLError::Settings* mOldSettings; +    LLError::Recorder* mProxy; +}; + +inline +std::ostream& operator<<(std::ostream& out, const CaptureLog& log) +{ +    return log.streamto(out); +} +  #endif /* ! defined(LL_WRAPLLERRS_H) */ | 
