From f0dbb878337082d3f581874c12e6df2f4659a464 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 20 Jan 2012 18:10:40 -0500 Subject: Per Richard, replace LLProcessLauncher with LLProcess. LLProcessLauncher had the somewhat fuzzy mandate of (1) accumulating parameters with which to launch a child process and (2) sometimes tracking the lifespan of the ensuing child process. But a valid LLProcessLauncher object might or might not have ever been associated with an actual child process. LLProcess specifically tracks a child process. In effect, it's a fairly thin wrapper around a process HANDLE (on Windows) or pid_t (elsewhere), with lifespan management thrown in. A static LLProcess::create() method launches a new child; create() accepts an LLSD bundle with child parameters. So building up a parameter bundle is deferred to LLSD rather than conflated with the process management object. Reconcile all known LLProcessLauncher consumers in the viewer code base, notably the class unit tests. --- indra/llcommon/tests/llprocess_test.cpp | 706 ++++++++++++++++++++++++++++++++ 1 file changed, 706 insertions(+) create mode 100644 indra/llcommon/tests/llprocess_test.cpp (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp new file mode 100644 index 0000000000..55e22abd81 --- /dev/null +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -0,0 +1,706 @@ +/** + * @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 +#include +// std headers +#include +// external library headers +#include "llapr.h" +#include "apr_thread_proc.h" +#include +#include +#include +#include +//#include +//#include +// other Linden headers +#include "../test/lltut.h" +#include "../test/manageapr.h" +#include "../test/namedtempfile.h" +#include "stringize.h" +#include "llsdutil.h" + +#if defined(LL_WINDOWS) +#define sleep(secs) _sleep((secs) * 1000) +#define EOL "\r\n" +#else +#define EOL "\n" +#include +#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 " + */ +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; +} + +/** + * 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 + 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["executable"] = PYTHON; + mParams["args"].append(mScript.getName()); + } + + /// Run Python script and wait for it to complete. + void run() + { + mPy = LLProcess::create(mParams); + tut::ensure(STRINGIZE("Couldn't launch " << mDesc << " script"), mPy); + // 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. + while (mPy->isRunning()) + { + sleep(1); + } + } + + /** + * 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"].append(out.getName()); + run(); + // assuming the script wrote to that file, read it + return readfile(out.getName(), STRINGIZE("from " << mDesc << " script")); + } + + LLSD mParams; + LLProcessPtr mPy; + std::string mDesc; + NamedTempFile mScript; +}; + +/// convenience function for PythonProcessLauncher::run() +template +static void python(const std::string& desc, const CONTENT& script) +{ + PythonProcessLauncher py(desc, script); + py.run(); +} + +/// convenience function for PythonProcessLauncher::run_read() +template +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.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_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(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 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 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 DescFile; + typedef std::list 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("getcwd()", + "from __future__ import with_statement\n" + "import os, sys\n" + "with open(sys.argv[1], 'w') as f:\n" + " f.write(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("args", + "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"].append("first arg"); // sys.argv[1] + py.mParams["args"].append("second arg"); // sys.argv[2] + // run_read() appends() one more argument, hence [3] + std::string output(py.run_read()); + boost::split_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("explicit kill()"); + PythonProcessLauncher py("kill()", + "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"].append(out.getName()); + py.mPy = LLProcess::create(py.mParams); + ensure("couldn't launch kill() script", py.mPy); + // Wait for the script to wake up and do its first write + int i = 0, timeout = 60; + for ( ; i < timeout; ++i) + { + sleep(1); + 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. + while (py.mPy->isRunning()) + { + sleep(1); + } + // 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("kill() script output", readfile(out.getName()), "ok"); + } + + template<> template<> + void object::test<5>() + { + set_test_name("implicit kill()"); + NamedTempFile out("out", "not started"); + LLProcess::id pid(0); + { + PythonProcessLauncher py("kill()", + "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"].append(out.getName()); + py.mPy = LLProcess::create(py.mParams); + ensure("couldn't launch kill() script", py.mPy); + // Capture id for later + pid = py.mPy->getProcessID(); + // Wait for the script to wake up and do its first write + int i = 0, timeout = 60; + for ( ; i < timeout; ++i) + { + sleep(1); + 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. + while (LLProcess::isRunning(pid)) + { + sleep(1); + } + // 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("kill() script output", readfile(out.getName()), "ok"); + } + + template<> template<> + void object::test<6>() + { + set_test_name("autokill"); + NamedTempFile from("from", "not started"); + NamedTempFile to("to", ""); + LLProcess::id pid(0); + { + PythonProcessLauncher py("autokill", + "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"].append(from.getName()); + py.mParams["args"].append(to.getName()); + py.mParams["autokill"] = false; + py.mPy = LLProcess::create(py.mParams); + ensure("couldn't launch kill() script", py.mPy); + // Capture id for later + pid = py.mPy->getProcessID(); + // Wait for the script to wake up and do its first write + int i = 0, timeout = 60; + for ( ; i < timeout; ++i) + { + sleep(1); + 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 + sleep(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. + while (LLProcess::isRunning(pid)) + { + sleep(1); + } + // If the LLProcess destructor implicitly called kill(), the + // script could not have written 'ack' as we expect. + ensure_equals("autokill script output", readfile(from.getName()), "ack"); + } +} // namespace tut -- cgit v1.2.3 From 47d94757075e338c480ba4d7d24948242a85a9bb Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Sat, 21 Jan 2012 11:45:15 -0500 Subject: Convert LLProcess consumers from LLSD to LLProcess::Params block. Using a Params block gives compile-time checking against attribute typos. One might inadvertently set myLLSD["autofill"] = false and only discover it when things behave strangely at runtime; but trying to set myParams.autofill will produce a compile error. However, it's excellent that the same LLProcess::create() method can accept either LLProcess::Params or a properly-constructed LLSD block. --- indra/llcommon/tests/llprocess_test.cpp | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index 55e22abd81..405540e436 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -107,8 +107,8 @@ struct PythonProcessLauncher const char* PYTHON(getenv("PYTHON")); tut::ensure("Set $PYTHON to the Python interpreter", PYTHON); - mParams["executable"] = PYTHON; - mParams["args"].append(mScript.getName()); + mParams.executable = PYTHON; + mParams.args.add(mScript.getName()); } /// Run Python script and wait for it to complete. @@ -142,13 +142,13 @@ struct PythonProcessLauncher { NamedTempFile out("out", ""); // placeholder // pass name of this temporary file to the script - mParams["args"].append(out.getName()); + mParams.args.add(out.getName()); run(); // assuming the script wrote to that file, read it return readfile(out.getName(), STRINGIZE("from " << mDesc << " script")); } - LLSD mParams; + LLProcess::Params mParams; LLProcessPtr mPy; std::string mDesc; NamedTempFile mScript; @@ -515,7 +515,7 @@ namespace tut "with open(sys.argv[1], 'w') as f:\n" " f.write(os.getcwd())\n"); // Before running, call setWorkingDirectory() - py.mParams["cwd"] = tempdir.getName(); + py.mParams.cwd = tempdir.getName(); ensure_equals("os.getcwd()", py.run_read(), tempdir.getName()); } @@ -531,9 +531,9 @@ namespace tut " 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"].append("first arg"); // sys.argv[1] - py.mParams["args"].append("second arg"); // sys.argv[2] + // 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 @@ -568,7 +568,7 @@ namespace tut "with open(sys.argv[1], 'w') as f:\n" " f.write('bad')\n"); NamedTempFile out("out", "not started"); - py.mParams["args"].append(out.getName()); + py.mParams.args.add(out.getName()); py.mPy = LLProcess::create(py.mParams); ensure("couldn't launch kill() script", py.mPy); // Wait for the script to wake up and do its first write @@ -611,7 +611,7 @@ namespace tut "# 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"].append(out.getName()); + py.mParams.args.add(out.getName()); py.mPy = LLProcess::create(py.mParams); ensure("couldn't launch kill() script", py.mPy); // Capture id for later @@ -667,9 +667,9 @@ namespace tut "# okay, saw 'go', write 'ack'\n" "with open(sys.argv[1], 'w') as f:\n" " f.write('ack')\n"); - py.mParams["args"].append(from.getName()); - py.mParams["args"].append(to.getName()); - py.mParams["autokill"] = false; + py.mParams.args.add(from.getName()); + py.mParams.args.add(to.getName()); + py.mParams.autokill = false; py.mPy = LLProcess::create(py.mParams); ensure("couldn't launch kill() script", py.mPy); // Capture id for later -- cgit v1.2.3 From 85581eefa63d8f8e8c5132c4cd7e137f6cb88869 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Mon, 30 Jan 2012 12:11:44 -0500 Subject: Expose 'handle' as well as 'id' on LLProcess objects. On Posix, these and the corresponding getProcessID()/getProcessHandle() accessors produce the same pid_t value; but on Windows, it's useful to distinguish an int-like 'id' useful to human log readers versus an opaque 'handle' for passing to platform-specific API functions. So make the distinction in a platform-independent way. --- indra/llcommon/tests/llprocess_test.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index 405540e436..4ad45bdf27 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -599,7 +599,7 @@ namespace tut { set_test_name("implicit kill()"); NamedTempFile out("out", "not started"); - LLProcess::id pid(0); + LLProcess::handle phandle(0); { PythonProcessLauncher py("kill()", "from __future__ import with_statement\n" @@ -614,8 +614,8 @@ namespace tut py.mParams.args.add(out.getName()); py.mPy = LLProcess::create(py.mParams); ensure("couldn't launch kill() script", py.mPy); - // Capture id for later - pid = py.mPy->getProcessID(); + // 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) @@ -630,7 +630,7 @@ namespace tut // Destroy the LLProcess, which should kill the child. } // wait for the script to terminate... one way or another. - while (LLProcess::isRunning(pid)) + while (LLProcess::isRunning(phandle)) { sleep(1); } @@ -646,7 +646,7 @@ namespace tut set_test_name("autokill"); NamedTempFile from("from", "not started"); NamedTempFile to("to", ""); - LLProcess::id pid(0); + LLProcess::handle phandle(0); { PythonProcessLauncher py("autokill", "from __future__ import with_statement\n" @@ -672,8 +672,8 @@ namespace tut py.mParams.autokill = false; py.mPy = LLProcess::create(py.mParams); ensure("couldn't launch kill() script", py.mPy); - // Capture id for later - pid = py.mPy->getProcessID(); + // 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) @@ -695,7 +695,7 @@ namespace tut outf << "go"; } // flush and close. // now wait for the script to terminate... one way or another. - while (LLProcess::isRunning(pid)) + while (LLProcess::isRunning(phandle)) { sleep(1); } -- cgit v1.2.3 From aafb03b29f5166e8978931ad8b717be32d942836 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 7 Feb 2012 10:53:23 -0500 Subject: Convert LLProcess implementation from platform-specific to using APR. Include logic to engage Linden apr_procattr_autokill_set() extension: on Windows, magic CreateProcess() flag must be pushed down into apr_proc_create() level. When using an APR package without that extension, present implementation should lock (e.g.) SLVoice.exe lifespan to viewer's on Windows XP but probably won't on Windows 7: need magic flag on CreateProcess(). Using APR child-termination callback requires us to define state (e.g. LLProcess::RUNNING). Take the opportunity to present Status, capturing state and (if terminated) rc or signal number; but since most of the time all caller really wants is to log the outcome, also present status string, encapsulating logic to examine state and describe exited-with-rc vs. killed-by-signal. New Status logic may report clearer results in the case of a Windows child process killed by exception. Clarify that static LLProcess::isRunning(handle) overload is only for use when the original LLProcess object has been destroyed: really only for unit tests. We necessarily retain our original platform-specific implementations for just that one method. (Nonstatic isRunning() no longer calls static method.) Clarify log output from llprocess_test.cpp in a couple places. --- indra/llcommon/tests/llprocess_test.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index 4ad45bdf27..60ed12ad6a 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -630,7 +630,7 @@ namespace tut // Destroy the LLProcess, which should kill the child. } // wait for the script to terminate... one way or another. - while (LLProcess::isRunning(phandle)) + while (LLProcess::isRunning(phandle, "kill() script")) { sleep(1); } @@ -643,7 +643,7 @@ namespace tut template<> template<> void object::test<6>() { - set_test_name("autokill"); + set_test_name("autokill=false"); NamedTempFile from("from", "not started"); NamedTempFile to("to", ""); LLProcess::handle phandle(0); @@ -695,7 +695,7 @@ namespace tut outf << "go"; } // flush and close. // now wait for the script to terminate... one way or another. - while (LLProcess::isRunning(phandle)) + while (LLProcess::isRunning(phandle, "autokill script")) { sleep(1); } -- cgit v1.2.3 From 32e11494ffde368b2ac166e16dc294d66a18492f Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 7 Feb 2012 12:28:57 -0500 Subject: Use os.path.normcase(os.path.normpath()) when comparing directories. Once again we've been bitten by comparison failure between "c:\somepath" and "C:\somepath". Normalize paths in both Python helper scripts to make that comparison more robust. --- indra/llcommon/tests/llprocess_test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index 60ed12ad6a..6d2292fb88 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -186,7 +186,7 @@ public: "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.realpath(tempfile.mkdtemp()))\n")) + " f.write(os.path.normcase(os.path.normpath(os.path.realpath(tempfile.mkdtemp()))))\n")) {} ~NamedTempDir() @@ -513,7 +513,7 @@ namespace tut "from __future__ import with_statement\n" "import os, sys\n" "with open(sys.argv[1], 'w') as f:\n" - " f.write(os.getcwd())\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()); -- cgit v1.2.3 From d4f887e43ccf0a8b7a84ebbfe6889462a1d9c25f Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Mon, 13 Feb 2012 16:18:46 -0500 Subject: Add unit tests for LLProcess::Status functionality. --- indra/llcommon/tests/llprocess_test.cpp | 46 +++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index 6d2292fb88..711715e373 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -555,6 +555,41 @@ namespace tut template<> template<> void object::test<4>() + { + set_test_name("exit(0)"); + PythonProcessLauncher py("exit(0)", + "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("exit(2)", + "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("syntax_error:", + "syntax_error:\n"); + py.run(); + ensure_equals("Status.mState", py.mPy->getStatus().mState, LLProcess::EXITED); + ensure_equals("Status.mData", py.mPy->getStatus().mData, 1); + } + + template<> template<> + void object::test<7>() { set_test_name("explicit kill()"); PythonProcessLauncher py("kill()", @@ -588,6 +623,13 @@ namespace tut { sleep(1); } +#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. @@ -595,7 +637,7 @@ namespace tut } template<> template<> - void object::test<5>() + void object::test<8>() { set_test_name("implicit kill()"); NamedTempFile out("out", "not started"); @@ -641,7 +683,7 @@ namespace tut } template<> template<> - void object::test<6>() + void object::test<9>() { set_test_name("autokill=false"); NamedTempFile from("from", "not started"); -- cgit v1.2.3 From aae61392be822218cabcab91d95eb1e75d471764 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Mon, 13 Feb 2012 17:38:25 -0500 Subject: Use per-frame ticks on "mainloop" LLEventPump to update LLProcess. When we reimplemented LLProcess on APR, necessitating APR's funny callback mechanism to sense child-process status, every isRunning() or getStatus() call called the APR poll function that calls ALL registered LLProcess callbacks. In other words, every time any consumer called any LLProcess::isRunning() method, all LLProcess callbacks were redundantly fired. Change that so that the single APR poll function is called once per frame, courtesy of the "mainloop" LLEventPump. Once per viewer frame should be well within the realtime duration in which it's reasonable to expect child-process status to change. In effect, this changes LLProcess's public API to introduce a dependency on "mainloop" ticks. Add such ticks to llprocess_test.cpp as well. --- indra/llcommon/tests/llprocess_test.cpp | 42 ++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 16 deletions(-) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index 711715e373..8c21be196b 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -33,6 +33,7 @@ #include "../test/namedtempfile.h" #include "stringize.h" #include "llsdutil.h" +#include "llevents.h" #if defined(LL_WINDOWS) #define sleep(secs) _sleep((secs) * 1000) @@ -88,6 +89,27 @@ static std::string readfile(const std::string& pathname, const std::string& desc 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 waitfor(LLProcess& proc) +{ + while (proc.isRunning()) + { + sleep(1); + LLEventPumps::instance().obtain("mainloop").post(LLSD()); + } +} + +void waitfor(LLProcess::handle h, const std::string& desc) +{ + while (LLProcess::isRunning(h, desc)) + { + sleep(1); + LLEventPumps::instance().obtain("mainloop").post(LLSD()); + } +} + /** * Construct an LLProcess to run a Python script. */ @@ -120,10 +142,7 @@ struct PythonProcessLauncher // there's no API to wait for the child to terminate -- but given // its use in our graphics-intensive interactive viewer, it's // understandable. - while (mPy->isRunning()) - { - sleep(1); - } + waitfor(*mPy); } /** @@ -619,10 +638,7 @@ namespace tut // script has performed its first write and should now be sleeping. py.mPy->kill(); // wait for the script to terminate... one way or another. - while (py.mPy->isRunning()) - { - sleep(1); - } + 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); @@ -672,10 +688,7 @@ namespace tut // Destroy the LLProcess, which should kill the child. } // wait for the script to terminate... one way or another. - while (LLProcess::isRunning(phandle, "kill() script")) - { - sleep(1); - } + 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. @@ -737,10 +750,7 @@ namespace tut outf << "go"; } // flush and close. // now wait for the script to terminate... one way or another. - while (LLProcess::isRunning(phandle, "autokill script")) - { - sleep(1); - } + waitfor(phandle, "autokill script"); // If the LLProcess destructor implicitly called kill(), the // script could not have written 'ack' as we expect. ensure_equals("autokill script output", readfile(from.getName()), "ack"); -- cgit v1.2.3 From e239cad1f509e3d96011acb61614f2481c46af38 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 15 Feb 2012 10:07:09 -0500 Subject: 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! --- indra/llcommon/tests/llprocess_test.cpp | 171 ++++++++++++++++++++++++++++++-- 1 file changed, 163 insertions(+), 8 deletions(-) (limited to 'indra/llcommon/tests/llprocess_test.cpp') 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::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 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 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 -- cgit v1.2.3 From c6ccdb5b5088f3ab24bb89d8c56049c7a17a663e Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 15 Feb 2012 12:11:38 -0500 Subject: Add tests for LLProcess::get[Opt][Read|Write]Pipe() validations. --- indra/llcommon/tests/llprocess_test.cpp | 90 +++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 3 deletions(-) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index d4e9977e63..06f9f3827f 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -900,14 +900,98 @@ namespace tut ensure_contains("did not name 'stderr'", message, "stderr"); } + template<> template<> + void object::test<14>() + { + set_test_name("internal pipe name warning"); + TestRecorder recorder; + PythonProcessLauncher py("pipe warning", + "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("did not log pipe name warning", ! message.empty()); + ensure_contains("log message did not mention internal pipe name", + message, "somename"); + } + +#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 (const EXCEPTION& e) \ + { \ + (THREW) = e.what(); \ + } \ + ensure("failed to throw " #EXCEPTION ": " #CODE, ! (THREW).empty()); \ + } while (0) + +#define EXPECT_FAIL_WITH_LOG(EXPECT, CODE) \ + do \ + { \ + TestRecorder recorder; \ + ensure(#CODE " succeeded", ! (CODE)); \ + ensure("wrong log message", ! recorder.messageWith(EXPECT).empty()); \ + } while (0) + + template<> template<> + void object::test<15>() + { + set_test_name("get*Pipe() validation"); + PythonProcessLauncher py("just stderr", + "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 + } + // 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 -- cgit v1.2.3 From 10ab4adc86207f86df30ab23d8858c23e7f550ea Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 15 Feb 2012 13:44:43 -0500 Subject: Fix llprocess_test.cpp's exception catching for Linux. In the course of re-enabling the indra/test tests last year, Log generalized a workaround I'd introduced in llsdmessage_test.cpp. In Linux viewer land, a test program trying to catch an expected exception can't seem to catch it by its specific class (across the libllcommon.so boundary), but must instead catch std::runtime_error and validate the typeid().name() string. Log added a macro for this idiom in llevents_tut.cpp. Generalize that macro further for normal-case processing as well, move it to a header file of its own and use it in all known places -- plus the new exception-catching tests in llprocess_test.cpp. --- indra/llcommon/tests/llprocess_test.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index 06f9f3827f..a901c577d6 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -31,6 +31,7 @@ #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" @@ -952,10 +953,7 @@ namespace tut { \ CODE; \ } \ - catch (const EXCEPTION& e) \ - { \ - (THREW) = e.what(); \ - } \ + CATCH_AND_STORE_WHAT_IN(THREW, EXCEPTION) \ ensure("failed to throw " #EXCEPTION ": " #CODE, ! (THREW).empty()); \ } while (0) -- cgit v1.2.3 From 56d931216e67a3e59199669bba022c65a9617bb5 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 15 Feb 2012 15:47:03 -0500 Subject: Add LLProcess::ReadPipe::size(), peek(), contains(). Also add "len" key to event data on LLProcess::getPump(). If you've used setLimit(), event["data"].length() may not reflect the length of the accumulated data in the ReadPipe. Add unit test with stdin/stdout handshake with child process. --- indra/llcommon/tests/llprocess_test.cpp | 43 +++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index a901c577d6..2db17cae97 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -919,6 +919,7 @@ namespace tut message, "somename"); } + /*-------------- support for "get*Pipe() validation" test --------------*/ #define TEST_getPipe(PROCESS, GETPIPE, GETOPTPIPE, VALID, NOPIPE, BADPIPE) \ do \ { \ @@ -985,11 +986,49 @@ namespace tut LLProcess::STDIN); // BADPIPE } + template<> template<> + void object::test<16>() + { + set_test_name("talk to stdin/stdout"); + PythonProcessLauncher py("stdin/stdout", + "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.mPy = LLProcess::create(py.mParams); + ensure("couldn't launch stdin/stdout script", py.mPy); + 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); + std::string line; + std::getline(childout.get_istream(), line); + ensure_equals("bad wakeup from stdin/stdout script", line, "ok"); + 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")); + std::getline(childout.get_istream(), line); + ensure_equals("child didn't ack", line, "ack"); + ensure_equals("bad child termination", py.mPy->getStatus().mState, LLProcess::EXITED); + ensure_equals("bad child exit code", py.mPy->getStatus().mData, 0); + } + // TODO: - // 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 peek() with substr } // namespace tut -- cgit v1.2.3 From fc6d70db8771320f3b1136e76383fc85ddf1b6b2 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 15 Feb 2012 20:57:25 -0500 Subject: Don't be confused by "\r\n" line endings on pipe on Windows. These are all very well when we just want to dump the output to a log, or whatever, but in a unit-test context it matters for comparison. --- indra/llcommon/tests/llprocess_test.cpp | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index 2db17cae97..6d6b888471 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -288,6 +288,22 @@ public: LLError::Settings* mOldSettings; }; +std::string getline(std::istream& in) +{ + std::string line; + std::getline(in, line); + // Blur the distinction between "\r\n" and plain "\n". std::getline() will + // have eaten the "\n", but we could still end up with a trailing "\r". + std::string::size_type lastpos = line.find_last_not_of("\r"); + if (lastpos != std::string::npos) + { + // Found at least one character that's not a trailing '\r'. SKIP OVER + // IT and then erase the rest of the line. + line.erase(lastpos+1); + } + return line; +} + /***************************************************************************** * TUT *****************************************************************************/ @@ -1010,17 +1026,15 @@ namespace tut yield(); } ensure("script never started", i < timeout); - std::string line; - std::getline(childout.get_istream(), line); - ensure_equals("bad wakeup from stdin/stdout script", line, "ok"); + ensure_equals("bad wakeup from stdin/stdout script", + getline(childout.get_istream()), "ok"); 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")); - std::getline(childout.get_istream(), line); - ensure_equals("child didn't ack", line, "ack"); + ensure_equals("child didn't ack", getline(childout.get_istream()), "ack"); ensure_equals("bad child termination", py.mPy->getStatus().mState, LLProcess::EXITED); ensure_equals("bad child exit code", py.mPy->getStatus().mData, 0); } -- cgit v1.2.3 From 85057908c3f7e48f1dc086ea1c82e672674b2596 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 15 Feb 2012 21:55:53 -0500 Subject: Add unit test for listening on LLProcess::ReadPipe::getPump(). --- indra/llcommon/tests/llprocess_test.cpp | 89 ++++++++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index 6d6b888471..31bc833a1d 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -1028,6 +1028,7 @@ namespace tut ensure("script never started", i < timeout); ensure_equals("bad wakeup from stdin/stdout script", getline(childout.get_istream()), "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) { @@ -1039,8 +1040,94 @@ namespace tut 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 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("ReadPipe listener", + "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.mPy = LLProcess::create(py.mParams); + ensure("couldn't launch ReadPipe listener script", py.mPy); + std::ostream& childin(py.mPy->getWritePipe(LLProcess::STDIN).get_ostream()); + LLProcess::ReadPipe& childout(py.mPy->getReadPipe(LLProcess::STDOUT)); + // 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 + for (i = 0; i < timeout && py.mPy->isRunning(); ++i) + { + yield(); + } + ensure("child took too long to terminate", i < timeout); + // now verify history + std::list::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); + } + // TODO: - // test listening on getReadPipe().getPump(), disconnecting // test setLimit(), getLimit() // test EOF -- check logging // test peek() with substr -- cgit v1.2.3 From e92c3113545dd60fb76e115da201163e340c730c Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 16 Feb 2012 16:05:04 -0500 Subject: Add LLProcess::ReadPipe::find() methods, with corresponding npos. If it's useful to have contains() to tell you whether incoming data contains a particular substring, and if it's useful for contains() and peek() to accept an offset within that data, then it's useful to allow you to get the offset of a desired substring within that data. But of course a find() returning offset needs something like std::string::npos for "not found"; borrow that convention. Support both find(const std::string&) and find(char); the latter permits a more efficient implementation. In fact, make find(string) recognize a string of length 1 and leverage the find(char) implementation. Given that, reimplement contains(mumble) as shorthand for find(mumble) != npos. Implement find() overloads using std::search() and std::find() on boost::asio::streambuf character iterators, rather than copying to std::string and then using string search like previous contains() implementation. Reimplement WritePipeImpl::tick() and ReadPipeImpl::tick() to write/read directly from/to boost::asio::streambuf data, instead of copying to/from a temporary flat buffer. As long as ReadPipeImpl::tick() keeps successfully filling buffers, keep reading. Previous implementation would only handle a long child write over successive tick() calls. Stop on read error or when we come up short. --- indra/llcommon/tests/llprocess_test.cpp | 2 ++ 1 file changed, 2 insertions(+) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index 31bc833a1d..d7bda34923 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -1131,5 +1131,7 @@ namespace tut // test setLimit(), getLimit() // test EOF -- check logging // test peek() with substr + // test contains(char) + // test find(string, offset), find(char, offset), offset <, =, > size() } // namespace tut -- cgit v1.2.3 From 4ecf9d6a2d981ef27c7b5bddc4807c67d12d5984 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 16 Feb 2012 16:40:14 -0500 Subject: Add unit test for LLProcess::ReadPipe::setLimit(). --- indra/llcommon/tests/llprocess_test.cpp | 62 +++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 18 deletions(-) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index d7bda34923..29233d4415 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -140,11 +140,17 @@ struct PythonProcessLauncher mParams.args.add(mScript.getName()); } - /// Run Python script and wait for it to complete. - void run() + /// 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 @@ -718,8 +724,7 @@ namespace tut " f.write('bad')\n"); NamedTempFile out("out", "not started"); py.mParams.args.add(out.getName()); - py.mPy = LLProcess::create(py.mParams); - ensure("couldn't launch kill() script", py.mPy); + py.launch(); // Wait for the script to wake up and do its first write int i = 0, timeout = 60; for ( ; i < timeout; ++i) @@ -765,8 +770,7 @@ namespace tut "with open(sys.argv[1], 'w') as f:\n" " f.write('bad')\n"); py.mParams.args.add(out.getName()); - py.mPy = LLProcess::create(py.mParams); - ensure("couldn't launch kill() script", py.mPy); + py.launch(); // Capture handle for later phandle = py.mPy->getProcessHandle(); // Wait for the script to wake up and do its first write @@ -820,8 +824,7 @@ namespace tut py.mParams.args.add(from.getName()); py.mParams.args.add(to.getName()); py.mParams.autokill = false; - py.mPy = LLProcess::create(py.mParams); - ensure("couldn't launch kill() script", py.mPy); + py.launch(); // Capture handle for later phandle = py.mPy->getProcessHandle(); // Wait for the script to wake up and do its first write @@ -1017,8 +1020,7 @@ namespace tut "print 'ack'\n"); py.mParams.files.add(LLProcess::FileParam("pipe")); // stdin py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout - py.mPy = LLProcess::create(py.mParams); - ensure("couldn't launch stdin/stdout script", py.mPy); + 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) @@ -1082,8 +1084,7 @@ namespace tut "sys.stdout.write('second line\\n')\n"); py.mParams.files.add(LLProcess::FileParam("pipe")); // stdin py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout - py.mPy = LLProcess::create(py.mParams); - ensure("couldn't launch ReadPipe listener script", py.mPy); + py.launch(); std::ostream& childin(py.mPy->getWritePipe(LLProcess::STDIN).get_ostream()); LLProcess::ReadPipe& childout(py.mPy->getReadPipe(LLProcess::STDOUT)); // listen for incoming data on childout @@ -1102,11 +1103,7 @@ namespace tut // disconnect from listener listener.mConnection.disconnect(); // finish out the run - for (i = 0; i < timeout && py.mPy->isRunning(); ++i) - { - yield(); - } - ensure("child took too long to terminate", i < timeout); + waitfor(*py.mPy); // now verify history std::list::const_iterator li(listener.mHistory.begin()), lend(listener.mHistory.end()); @@ -1127,8 +1124,37 @@ namespace tut ensure("more than 3 events", li == lend); } + template<> template<> + void object::test<18>() + { + set_test_name("setLimit()"); + PythonProcessLauncher py("setLimit()", + "import sys\n" + "print 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() + sizeof(EOL) - 1); + ensure_equals("length of setLimit(10) data", + listener.mHistory.back()["data"].asString().length(), 10); + } + // TODO: - // test setLimit(), getLimit() // test EOF -- check logging // test peek() with substr // test contains(char) -- cgit v1.2.3 From a06ba836c76ea8b35aeca9d09bd7d3b043a4c962 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 16 Feb 2012 17:35:34 -0500 Subject: Fix bug in LLProcess::ReadPipe::peek() substring computation. Add unit tests for peek() with substring args, reimplemented contains(), various forms of find(). (yay unit tests) --- indra/llcommon/tests/llprocess_test.cpp | 61 +++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 6 deletions(-) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index 29233d4415..e5d873c8ee 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -1130,7 +1130,7 @@ namespace tut set_test_name("setLimit()"); PythonProcessLauncher py("setLimit()", "import sys\n" - "print sys.argv[1]\n"); + "sys.stdout.write(sys.argv[1])\n"); std::string abc("abcdefghijklmnopqrstuvwxyz"); py.mParams.args.add(abc); py.mParams.files.add(LLProcess::FileParam()); // stdin @@ -1148,16 +1148,65 @@ namespace tut // 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() + sizeof(EOL) - 1); + 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<19>() + { + set_test_name("peek() ReadPipe data"); + PythonProcessLauncher py("peek() ReadPipe", + "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); + } + // TODO: // test EOF -- check logging - // test peek() with substr - // test contains(char) - // test find(string, offset), find(char, offset), offset <, =, > size() } // namespace tut -- cgit v1.2.3 From f52cf4be7003f18813da31b25d204f10eb36db17 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 16 Feb 2012 21:10:06 -0500 Subject: Fix typos in a few LLProcess::ReadPipe::find() unit tests. The typos didn't make for invalid tests, but they made a few tests redundant while leaving other (subtly different) cases untested. --- indra/llcommon/tests/llprocess_test.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index e5d873c8ee..c67605cc0b 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -1179,16 +1179,16 @@ namespace tut ensure("contains(\":\")", ! childout.contains(":")); ensure("contains(':')", ! childout.contains(':')); ensure("contains(\"d\")", childout.contains("d")); - 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')", 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_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 -- cgit v1.2.3 From 8b5d5f9652499103b966524e1c0ceef869e29eeb Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Mon, 20 Feb 2012 12:40:38 -0500 Subject: Make LLProcess post termination event to specified pump if desired. This way a caller need not spin on isRunning(); we can just listen for the requested termination event. Post a similar event containing error message if for any reason LLProcess::create() failed to launch the child. Add unit tests for both cases. --- indra/llcommon/tests/llprocess_test.cpp | 59 +++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index c67605cc0b..1a755c283c 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -1206,6 +1206,65 @@ namespace tut ensure("find(\"ghi\", 27)", childout.find("ghi", 27) == LLProcess::ReadPipe::npos); } + template<> template<> + void object::test<20>() + { + set_test_name("good postend"); + PythonProcessLauncher py("postend", + "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"); + } + + 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.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")); + // Ha ha, in this case the implementation normally sets "desc" to + // params.executable. But as the nature of the problem is that + // params.executable is empty, expecting "desc" to be nonempty is a + // bit unreasonable! + //ensure("desc empty", ! postend["desc"].asString().empty()); + 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()); + } + // TODO: // test EOF -- check logging -- cgit v1.2.3 From 999484a60896b11df1af9a44e58ccae6fa6ecbed Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Mon, 20 Feb 2012 14:22:32 -0500 Subject: Let LLProcess consumer specify desired description for logging. If caller runs (e.g.) a Python script, it's not very helpful to a human log reader to keep seeing LLProcess instances logged as /pathname/to/python (pid). If caller is aware, the code can at least use the script name as the desc -- or maybe even a hint as to the script's purpose. If caller doesn't explicitly pass a desc, at least shorten to just the basename of the executable. --- indra/llcommon/tests/llprocess_test.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index 1a755c283c..fe599e7892 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -136,6 +136,7 @@ struct PythonProcessLauncher 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()); } @@ -1244,17 +1245,14 @@ namespace tut std::string pumpname("postend"); EventListener listener(LLEventPumps::instance().obtain(pumpname)); LLProcess::Params params; + params.desc = "bad postend"; 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")); - // Ha ha, in this case the implementation normally sets "desc" to - // params.executable. But as the nature of the problem is that - // params.executable is empty, expecting "desc" to be nonempty is a - // bit unreasonable! - //ensure("desc empty", ! postend["desc"].asString().empty()); + 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"]); -- cgit v1.2.3 From 14ddc6474a0ae83db8d034b00138289fb15e41b7 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 23 Feb 2012 13:41:26 -0500 Subject: Tighten up LLProcess pipe support, per Richard's code review. Clarify wording in some of the doc comments; be a bit more explicit about some of the parameter fields. Make some query methods 'const'. Change default LLProcess::ReadPipe::getLimit() value to 0: don't post any incoming data with notification event unless caller requests it. But do post pertinent FILESLOT in case caller reuses same listener for both stdout and stderr. Use more idiomatic, readable syntax for accessing LLProcess::Params data. --- indra/llcommon/tests/llprocess_test.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index fe599e7892..d7feddd26b 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -696,7 +696,7 @@ namespace tut "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.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); @@ -1088,6 +1088,8 @@ namespace tut 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 -- cgit v1.2.3 From 7fd281ac9971caa1dfffd42e6ff16dd44da20179 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Mon, 27 Feb 2012 15:24:14 -0500 Subject: Reduce redundancy in llprocess_test.cpp using get_test_name(). --- indra/llcommon/tests/llprocess_test.cpp | 46 ++++++++++++++++----------------- 1 file changed, 23 insertions(+), 23 deletions(-) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index d7feddd26b..b02a5c0631 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -620,7 +620,7 @@ namespace tut // 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("getcwd()", + PythonProcessLauncher py(get_test_name(), "from __future__ import with_statement\n" "import os, sys\n" "with open(sys.argv[1], 'w') as f:\n" @@ -634,7 +634,7 @@ namespace tut void object::test<3>() { set_test_name("arguments"); - PythonProcessLauncher py("args", + PythonProcessLauncher py(get_test_name(), "from __future__ import with_statement\n" "import sys\n" // note nonstandard output-file arg! @@ -668,7 +668,7 @@ namespace tut void object::test<4>() { set_test_name("exit(0)"); - PythonProcessLauncher py("exit(0)", + PythonProcessLauncher py(get_test_name(), "import sys\n" "sys.exit(0)\n"); py.run(); @@ -680,7 +680,7 @@ namespace tut void object::test<5>() { set_test_name("exit(2)"); - PythonProcessLauncher py("exit(2)", + PythonProcessLauncher py(get_test_name(), "import sys\n" "sys.exit(2)\n"); py.run(); @@ -692,7 +692,7 @@ namespace tut void object::test<6>() { set_test_name("syntax_error:"); - PythonProcessLauncher py("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 @@ -713,7 +713,7 @@ namespace tut void object::test<7>() { set_test_name("explicit kill()"); - PythonProcessLauncher py("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" @@ -750,7 +750,7 @@ namespace tut // 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("kill() script output", readfile(out.getName()), "ok"); + ensure_equals(get_test_name() + " script output", readfile(out.getName()), "ok"); } template<> template<> @@ -760,7 +760,7 @@ namespace tut NamedTempFile out("out", "not started"); LLProcess::handle phandle(0); { - PythonProcessLauncher py("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" @@ -792,7 +792,7 @@ namespace tut // 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("kill() script output", readfile(out.getName()), "ok"); + ensure_equals(get_test_name() + " script output", readfile(out.getName()), "ok"); } template<> template<> @@ -803,7 +803,7 @@ namespace tut NamedTempFile to("to", ""); LLProcess::handle phandle(0); { - PythonProcessLauncher py("autokill", + PythonProcessLauncher py(get_test_name(), "from __future__ import with_statement\n" "import sys, time\n" "with open(sys.argv[1], 'w') as f:\n" @@ -852,7 +852,7 @@ namespace tut waitfor(phandle, "autokill script"); // If the LLProcess destructor implicitly called kill(), the // script could not have written 'ack' as we expect. - ensure_equals("autokill script output", readfile(from.getName()), "ack"); + ensure_equals(get_test_name() + " script output", readfile(from.getName()), "ack"); } template<> template<> @@ -860,7 +860,7 @@ namespace tut { set_test_name("'bogus' test"); TestRecorder recorder; - PythonProcessLauncher py("'bogus' test", + PythonProcessLauncher py(get_test_name(), "print 'Hello world'\n"); py.mParams.files.add(LLProcess::FileParam("bogus")); py.mPy = LLProcess::create(py.mParams); @@ -876,7 +876,7 @@ namespace tut set_test_name("'file' test"); // Replace this test with one or more real 'file' tests when we // implement 'file' support - PythonProcessLauncher py("'file' test", + PythonProcessLauncher py(get_test_name(), "print 'Hello world'\n"); py.mParams.files.add(LLProcess::FileParam()); py.mParams.files.add(LLProcess::FileParam("file")); @@ -891,7 +891,7 @@ namespace tut // Replace this test with one or more real 'tpipe' tests when we // implement 'tpipe' support TestRecorder recorder; - PythonProcessLauncher py("'tpipe' test", + PythonProcessLauncher py(get_test_name(), "print 'Hello world'\n"); py.mParams.files.add(LLProcess::FileParam()); py.mParams.files.add(LLProcess::FileParam("tpipe")); @@ -909,7 +909,7 @@ namespace tut // Replace this test with one or more real 'npipe' tests when we // implement 'npipe' support TestRecorder recorder; - PythonProcessLauncher py("'npipe' test", + PythonProcessLauncher py(get_test_name(), "print 'Hello world'\n"); py.mParams.files.add(LLProcess::FileParam()); py.mParams.files.add(LLProcess::FileParam()); @@ -926,7 +926,7 @@ namespace tut { set_test_name("internal pipe name warning"); TestRecorder recorder; - PythonProcessLauncher py("pipe warning", + PythonProcessLauncher py(get_test_name(), "import sys\n" "sys.exit(7)\n"); py.mParams.files.add(LLProcess::FileParam("pipe", "somename")); @@ -990,7 +990,7 @@ namespace tut void object::test<15>() { set_test_name("get*Pipe() validation"); - PythonProcessLauncher py("just stderr", + 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 @@ -1010,7 +1010,7 @@ namespace tut void object::test<16>() { set_test_name("talk to stdin/stdout"); - PythonProcessLauncher py("stdin/stdout", + PythonProcessLauncher py(get_test_name(), "import sys, time\n" "print 'ok'\n" "sys.stdout.flush()\n" @@ -1071,7 +1071,7 @@ namespace tut void object::test<17>() { set_test_name("listen for ReadPipe events"); - PythonProcessLauncher py("ReadPipe listener", + PythonProcessLauncher py(get_test_name(), "import sys\n" "sys.stdout.write('abc')\n" "sys.stdout.flush()\n" @@ -1131,7 +1131,7 @@ namespace tut void object::test<18>() { set_test_name("setLimit()"); - PythonProcessLauncher py("setLimit()", + PythonProcessLauncher py(get_test_name(), "import sys\n" "sys.stdout.write(sys.argv[1])\n"); std::string abc("abcdefghijklmnopqrstuvwxyz"); @@ -1160,7 +1160,7 @@ namespace tut void object::test<19>() { set_test_name("peek() ReadPipe data"); - PythonProcessLauncher py("peek() ReadPipe", + PythonProcessLauncher py(get_test_name(), "import sys\n" "sys.stdout.write(sys.argv[1])\n"); std::string abc("abcdefghijklmnopqrstuvwxyz"); @@ -1213,7 +1213,7 @@ namespace tut void object::test<20>() { set_test_name("good postend"); - PythonProcessLauncher py("postend", + PythonProcessLauncher py(get_test_name(), "import sys\n" "sys.exit(35)\n"); std::string pumpname("postend"); @@ -1247,7 +1247,7 @@ namespace tut std::string pumpname("postend"); EventListener listener(LLEventPumps::instance().obtain(pumpname)); LLProcess::Params params; - params.desc = "bad postend"; + params.desc = get_test_name(); params.postend = pumpname; LLProcessPtr child = LLProcess::create(params); ensure("shouldn't have launched", ! child); -- cgit v1.2.3 From 3649eda62ad3a04203e6c562e78815a95896bbd4 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 29 Feb 2012 17:10:19 -0500 Subject: Guarantee LLProcess::Params::postend listener any ReadPipe data. Previously one might get process-terminated notification but still have to wait for the child process's final data to arrive on one or more ReadPipes. That required complex consumer timing logic to handle incomplete pending ReadPipe data, e.g. a partial last line with no terminating newline. New code guarantees that by the time LLProcess sends process-terminated notification, all pending pipe data will have been buffered in ReadPipes. Document LLProcess::ReadPipe::getPump() notification event; add "eof" key. Add LLProcess::ReadPipe::getline() and read() convenience methods. Add static LLProcess::getline() and basename() convenience methods, publishing logic already present elsewhere. Use ReadPipe::getline() and read() in unit tests. Add unit test for "eof" event on ReadPipe::getPump(). Add unit test verifying that final data have been buffered by termination notification event. --- indra/llcommon/tests/llprocess_test.cpp | 139 ++++++++++++++++++++++---------- 1 file changed, 95 insertions(+), 44 deletions(-) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index b02a5c0631..3537133a47 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -295,22 +295,6 @@ public: LLError::Settings* mOldSettings; }; -std::string getline(std::istream& in) -{ - std::string line; - std::getline(in, line); - // Blur the distinction between "\r\n" and plain "\n". std::getline() will - // have eaten the "\n", but we could still end up with a trailing "\r". - std::string::size_type lastpos = line.find_last_not_of("\r"); - if (lastpos != std::string::npos) - { - // Found at least one character that's not a trailing '\r'. SKIP OVER - // IT and then erase the rest of the line. - line.erase(lastpos+1); - } - return line; -} - /***************************************************************************** * TUT *****************************************************************************/ @@ -1030,7 +1014,7 @@ namespace tut } ensure("script never started", i < timeout); ensure_equals("bad wakeup from stdin/stdout script", - getline(childout.get_istream()), "ok"); + 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) @@ -1038,7 +1022,7 @@ namespace tut yield(); } ensure("script never replied", childout.contains("\n")); - ensure_equals("child didn't ack", getline(childout.get_istream()), "ack"); + 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); } @@ -1129,6 +1113,32 @@ namespace tut 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::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(), @@ -1157,7 +1167,7 @@ namespace tut } template<> template<> - void object::test<19>() + void object::test<20>() { set_test_name("peek() ReadPipe data"); PythonProcessLauncher py(get_test_name(), @@ -1210,7 +1220,32 @@ namespace tut } template<> template<> - void object::test<20>() + 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(), @@ -1240,32 +1275,48 @@ namespace tut 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<21>() + void object::test<23>() { - set_test_name("bad postend"); + 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"); - 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()); + 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); } - - // TODO: - // test EOF -- check logging - } // namespace tut -- cgit v1.2.3 From 2596816f316e13b717bcdacddad0da48c90c3b6d Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 1 Mar 2012 13:43:52 -0500 Subject: Break out TestRecorder class as CaptureLog into wrapllerrs.h. Giving more unit tests the ability to capture and examine log output is generally useful. Renaming the class just makes it less ambiguous: what's a TestRecorder? Something that records tests? --- indra/llcommon/tests/llprocess_test.cpp | 74 +++------------------------------ 1 file changed, 6 insertions(+), 68 deletions(-) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index 3537133a47..c07a9d3925 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -35,7 +35,7 @@ #include "stringize.h" #include "llsdutil.h" #include "llevents.h" -#include "llerrorcontrol.h" +#include "wrapllerrs.h" #if defined(LL_WINDOWS) #define sleep(secs) _sleep((secs) * 1000) @@ -233,68 +233,6 @@ 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::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 MessageList; - MessageList mMessages; - LLError::Settings* mOldSettings; -}; - /***************************************************************************** * TUT *****************************************************************************/ @@ -843,7 +781,7 @@ namespace tut void object::test<10>() { set_test_name("'bogus' test"); - TestRecorder recorder; + CaptureLog recorder; PythonProcessLauncher py(get_test_name(), "print 'Hello world'\n"); py.mParams.files.add(LLProcess::FileParam("bogus")); @@ -874,7 +812,7 @@ namespace tut set_test_name("'tpipe' test"); // Replace this test with one or more real 'tpipe' tests when we // implement 'tpipe' support - TestRecorder recorder; + CaptureLog recorder; PythonProcessLauncher py(get_test_name(), "print 'Hello world'\n"); py.mParams.files.add(LLProcess::FileParam()); @@ -892,7 +830,7 @@ namespace tut set_test_name("'npipe' test"); // Replace this test with one or more real 'npipe' tests when we // implement 'npipe' support - TestRecorder recorder; + CaptureLog recorder; PythonProcessLauncher py(get_test_name(), "print 'Hello world'\n"); py.mParams.files.add(LLProcess::FileParam()); @@ -909,7 +847,7 @@ namespace tut void object::test<14>() { set_test_name("internal pipe name warning"); - TestRecorder recorder; + CaptureLog recorder; PythonProcessLauncher py(get_test_name(), "import sys\n" "sys.exit(7)\n"); @@ -965,7 +903,7 @@ namespace tut #define EXPECT_FAIL_WITH_LOG(EXPECT, CODE) \ do \ { \ - TestRecorder recorder; \ + CaptureLog recorder; \ ensure(#CODE " succeeded", ! (CODE)); \ ensure("wrong log message", ! recorder.messageWith(EXPECT).empty()); \ } while (0) -- cgit v1.2.3 From f904867720291bc63ea5e140194069d29f70fb40 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 1 Mar 2012 14:33:23 -0500 Subject: Make CaptureLog::withMessage() raise tut::failure if not found. All known callers were using ensure(! withMessage(...).empty()). Centralize that logic. Make failure message report the string being sought and the log messages in which it wasn't found. In case someone does want to permit the search to fail, add an optional 'required' parameter, default true. Leverage new functionality in llprocess_test.cpp. --- indra/llcommon/tests/llprocess_test.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index c07a9d3925..6103764f24 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -788,7 +788,6 @@ namespace tut 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"); } @@ -820,7 +819,6 @@ namespace tut 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"); } @@ -839,7 +837,6 @@ namespace tut 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"); } @@ -856,7 +853,6 @@ namespace tut 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("did not log pipe name warning", ! message.empty()); ensure_contains("log message did not mention internal pipe name", message, "somename"); } @@ -905,7 +901,7 @@ namespace tut { \ CaptureLog recorder; \ ensure(#CODE " succeeded", ! (CODE)); \ - ensure("wrong log message", ! recorder.messageWith(EXPECT).empty()); \ + recorder.messageWith(EXPECT); \ } while (0) template<> template<> -- cgit v1.2.3 From 7d3cf544c7962ae6cc8e0e8b8e8db817e366a9cc Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 13 Mar 2012 14:15:11 -0400 Subject: Add timeout functionality to waitfor() helper functions. Otherwise, a stuck child process could potentially hang the test, and thus the whole viewer build. --- indra/llcommon/tests/llprocess_test.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) (limited to 'indra/llcommon/tests/llprocess_test.cpp') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index 6103764f24..99186ed434 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -101,20 +101,26 @@ void yield(int seconds=1) LLEventPumps::instance().obtain("mainloop").post(LLSD()); } -void waitfor(LLProcess& proc) +void waitfor(LLProcess& proc, int timeout=60) { - while (proc.isRunning()) + 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) +void waitfor(LLProcess::handle h, const std::string& desc, int timeout=60) { - while (LLProcess::isRunning(h, desc)) + 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); } /** -- cgit v1.2.3