diff options
author | Nat Goodspeed <nat@lindenlab.com> | 2012-02-15 10:07:09 -0500 |
---|---|---|
committer | Nat Goodspeed <nat@lindenlab.com> | 2012-02-15 10:07:09 -0500 |
commit | e239cad1f509e3d96011acb61614f2481c46af38 (patch) | |
tree | b6b187c3d774c3f04410b89ce347b8c7dafdfdcd /indra/llcommon/tests | |
parent | aae61392be822218cabcab91d95eb1e75d471764 (diff) |
Preliminary pipe support for LLProcess.
Add LLProcess::FileParam to specify how to construct each child's standard
file slot, with lots of comments about features designed but not yet
implemented. The point is to design it with enough flexibility to be able to
extend to foreseeable use cases.
Add LLProcess::Params::files to collect up to 3 FileParam items. Naturally
this extends the accepted LLSD syntax as well.
Implement type="" (child inherits parent file descriptor) and "pipe" (parent
constructs anonymous pipe to pass to child).
Add LLProcess::FILESLOT enum, plus methods:
getReadPipe(FILESLOT), getOptReadPipe(FILESLOT)
getWritePipe(), getOptWritePipe()
getPipeName(FILESLOT): placeholder implementation for now
Add LLProcess::ReadPipe and WritePipe classes, as returned by get*Pipe().
WritePipe supports get_ostream() method for streaming to child stdin.
ReadPipe supports get_istream() method for reading from child stdout/stderr.
It also provides getPump() returning LLEventPump& so interested parties can
listen for arrival of new data on the aforementioned std::istream.
For "pipe" slots, instantiate appropriate *Pipe class.
ReadPipe and WritePipe classes are pure virtual bases for ReadPipeImpl and
WritePipeImpl, respectively: all implementation data are hidden in the latter
classes, visible only in llprocess.cpp. In fact each *PipeImpl class registers
itself for "mainloop" ticks, attempting nonblocking I/O to the underlying
apr_file_t on each tick. Data are buffered in a boost::asio::streambuf, which
bridges between std::[io]stream and the APR I/O calls.
Sanity-test ReadPipeImpl by using a pipe to absorb the Python "SyntaxError"
output from the successful syntax_error test, rather than alarming the user.
Add first few unit tests for validating FileParam. More tests coming!
Diffstat (limited to 'indra/llcommon/tests')
-rw-r--r-- | indra/llcommon/tests/llprocess_test.cpp | 171 |
1 files changed, 163 insertions, 8 deletions
diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index 8c21be196b..d4e9977e63 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -34,6 +34,7 @@ #include "stringize.h" #include "llsdutil.h" #include "llevents.h" +#include "llerrorcontrol.h" #if defined(LL_WINDOWS) #define sleep(secs) _sleep((secs) * 1000) @@ -92,12 +93,18 @@ static std::string readfile(const std::string& pathname, const std::string& desc /// 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) { while (proc.isRunning()) { - sleep(1); - LLEventPumps::instance().obtain("mainloop").post(LLSD()); + yield(); } } @@ -105,8 +112,7 @@ void waitfor(LLProcess::handle h, const std::string& desc) { while (LLProcess::isRunning(h, desc)) { - sleep(1); - LLEventPumps::instance().obtain("mainloop").post(LLSD()); + yield(); } } @@ -219,6 +225,68 @@ private: std::string mPath; }; +// statically reference the function in test.cpp... it's short, we could +// replicate, but better to reuse +extern void wouldHaveCrashed(const std::string& message); + +/** + * Capture log messages. This is adapted (simplified) from the one in + * llerror_test.cpp. Sigh, should've broken that out into a separate header + * file, but time for this project is short... + */ +class TestRecorder : public LLError::Recorder +{ +public: + TestRecorder(): + // 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()) + { + LLError::setFatalFunction(wouldHaveCrashed); + LLError::setDefaultLevel(LLError::LEVEL_DEBUG); + LLError::addRecorder(this); + } + + ~TestRecorder() + { + LLError::removeRecorder(this); + 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) + { + for (std::list<std::string>::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 + return std::string(); + } + + typedef std::list<std::string> MessageList; + MessageList mMessages; + LLError::Settings* mOldSettings; +}; + /***************************************************************************** * TUT *****************************************************************************/ @@ -602,9 +670,19 @@ namespace tut set_test_name("syntax_error:"); PythonProcessLauncher py("syntax_error:", "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("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<> @@ -629,7 +707,7 @@ namespace tut int i = 0, timeout = 60; for ( ; i < timeout; ++i) { - sleep(1); + yield(); if (readfile(out.getName(), "from kill() script") == "ok") break; } @@ -678,7 +756,7 @@ namespace tut int i = 0, timeout = 60; for ( ; i < timeout; ++i) { - sleep(1); + yield(); if (readfile(out.getName(), "from kill() script") == "ok") break; } @@ -733,7 +811,7 @@ namespace tut int i = 0, timeout = 60; for ( ; i < timeout; ++i) { - sleep(1); + yield(); if (readfile(from.getName(), "from autokill script") == "ok") break; } @@ -742,7 +820,7 @@ namespace tut // Now destroy the LLProcess, which should NOT kill the child! } // If the destructor killed the child anyway, give it time to die - sleep(2); + yield(2); // How do we know it's not terminated? By making it respond to // a specific stimulus in a specific way. { @@ -755,4 +833,81 @@ namespace tut // script could not have written 'ack' as we expect. ensure_equals("autokill script output", readfile(from.getName()), "ack"); } + + template<> template<> + void object::test<10>() + { + set_test_name("'bogus' test"); + TestRecorder recorder; + PythonProcessLauncher py("'bogus' test", + "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("did not log 'bogus' type", ! message.empty()); + 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("'file' test", + "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 + TestRecorder recorder; + PythonProcessLauncher py("'tpipe' test", + "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("did not log 'tpipe' type", ! message.empty()); + 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 + TestRecorder recorder; + PythonProcessLauncher py("'npipe' test", + "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("did not log 'npipe' type", ! message.empty()); + ensure_contains("did not name 'stderr'", message, "stderr"); + } + + // TODO: + // test "pipe" with nonempty name (should log & continue) + // test pipe for just stderr (tests for get[Opt]ReadPipe(), get[Opt]WritePipe()) + // test pipe for stdin, stdout (etc.) + // test getWritePipe().get_ostream(), getReadPipe().get_istream() + // test listening on getReadPipe().getPump(), disconnecting + // test setLimit(), getLimit() + // test EOF -- check logging + // test get(Read|Write)Pipe(3), unmonitored slot, getReadPipe(1), getWritePipe(0) + } // namespace tut |