summaryrefslogtreecommitdiff
path: root/indra/llcommon/tests
diff options
context:
space:
mode:
Diffstat (limited to 'indra/llcommon/tests')
-rw-r--r--indra/llcommon/tests/StringVec.h37
-rw-r--r--indra/llcommon/tests/listener.h21
-rw-r--r--indra/llcommon/tests/llerror_test.cpp226
-rw-r--r--indra/llcommon/tests/llinstancetracker_test.cpp69
-rw-r--r--indra/llcommon/tests/llleap_test.cpp694
-rw-r--r--indra/llcommon/tests/llprocess_test.cpp1262
-rw-r--r--indra/llcommon/tests/llsdserialize_test.cpp274
-rw-r--r--indra/llcommon/tests/llstreamqueue_test.cpp197
-rw-r--r--indra/llcommon/tests/llstring_test.cpp118
-rw-r--r--indra/llcommon/tests/setpython.py19
-rw-r--r--indra/llcommon/tests/wrapllerrs.h129
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) */