/** * @file llleap_test.cpp * @author Nat Goodspeed * @date 2012-02-21 * @brief Test for llleap. * * $LicenseInfo:firstyear=2012&license=viewerlgpl$ * Copyright (c) 2012, Linden Research, Inc. * $/LicenseInfo$ */ // Precompiled header #include "linden_common.h" // associated header #include "llleap.h" // STL headers // std headers // external library headers #include #include #include // other Linden headers #include "../test/lltut.h" #include "../test/namedtempfile.h" #include "../test/manageapr.h" #include "../test/catch_and_store_what_in.h" #include "wrapllerrs.h" #include "llevents.h" #include "llprocess.h" #include "stringize.h" #include "StringVec.h" using boost::assign::list_of; static ManageAPR manager; StringVec sv(const StringVec& listof) { return listof; } #if defined(LL_WINDOWS) #define sleep(secs) _sleep((secs) * 1000) #endif void waitfor(const std::vector& instances) { int i, timeout = 60; for (i = 0; i < timeout; ++i) { // Every iteration, test whether any of the passed LLLeap instances // still exist (are still running). std::vector::const_iterator vli(instances.begin()), vlend(instances.end()); for ( ; vli != vlend; ++vli) { // getInstance() returns NULL if it's terminated/gone, non-NULL if // it's still running if (LLLeap::getInstance(*vli)) break; } // If we made it through all of 'instances' without finding one that's // still running, we're done. if (vli == vlend) return; // Found an instance that's still running. Wait and pump LLProcess. sleep(1); LLEventPumps::instance().obtain("mainloop").post(LLSD()); } tut::ensure("timed out without terminating", i < timeout); } void waitfor(LLLeap* instance) { std::vector instances; instances.push_back(instance); waitfor(instances); } /***************************************************************************** * TUT *****************************************************************************/ namespace tut { struct llleap_data { llleap_data(): reader(".py", // This logic is adapted from vita.viewerclient.receiveEvent() "import sys\n" "LEFTOVER = ''\n" "class ProtocolError(Exception):\n" " pass\n" "def get():\n" " global LEFTOVER\n" " hdr = LEFTOVER\n" " if ':' not in hdr:\n" " hdr += sys.stdin.read(20)\n" " if not hdr:\n" " sys.exit(0)\n" " parts = hdr.split(':', 1)\n" " if len(parts) != 2:\n" " raise ProtocolError('Expected len:data, got %r' % hdr)\n" " try:\n" " length = int(parts[0])\n" " except ValueError:\n" " raise ProtocolError('Non-numeric len %r' % parts[0])\n" " del parts[0]\n" " received = len(parts[0])\n" " while received < length:\n" " parts.append(sys.stdin.read(length - received))\n" " received += len(parts[-1])\n" " if received > length:\n" " excess = length - received\n" " LEFTOVER = parts[-1][excess:]\n" " parts[-1] = parts[-1][:excess]\n" " data = ''.join(parts)\n" " assert len(data) == length\n" " return data\n"), // Get the actual pathname of the NamedExtTempFile and trim off // the ".py" extension. (We could cache reader.getName() in a // separate member variable, but I happen to know getName() just // returns a NamedExtTempFile member rather than performing any // computation, so I don't mind calling it twice.) Then take the // basename. reader_module(LLProcess::basename( reader.getName().substr(0, reader.getName().length()-3))), pPYTHON(getenv("PYTHON")), PYTHON(pPYTHON? pPYTHON : "") { ensure("Set PYTHON to interpreter pathname", pPYTHON); } NamedExtTempFile reader; const std::string reader_module; const char* pPYTHON; const std::string PYTHON; }; typedef test_group llleap_group; typedef llleap_group::object object; llleap_group llleapgrp("llleap"); template<> template<> void object::test<1>() { set_test_name("multiple LLLeap instances"); NamedTempFile script("py", "import time\n" "time.sleep(1)\n"); std::vector instances; instances.push_back(LLLeap::create(get_test_name(), sv(list_of(PYTHON)(script.getName())))); instances.push_back(LLLeap::create(get_test_name(), sv(list_of(PYTHON)(script.getName())))); // In this case we're simply establishing that two LLLeap instances // can coexist without throwing exceptions or bombing in any other // way. Wait for them to terminate. waitfor(instances); } template<> template<> void object::test<2>() { set_test_name("stderr to log"); NamedTempFile script("py", "import sys\n" "sys.stderr.write('''Hello from Python!\n" "note partial line''')\n"); CaptureLog log(LLError::LEVEL_INFO); waitfor(LLLeap::create(get_test_name(), sv(list_of(PYTHON)(script.getName())))); log.messageWith("Hello from Python!"); log.messageWith("note partial line"); } template<> template<> void object::test<3>() { set_test_name("empty plugin vector"); std::string threw; try { LLLeap::create("empty", StringVec()); } CATCH_AND_STORE_WHAT_IN(threw, LLLeap::Error) ensure_contains("LLLeap::Error", threw, "no plugin"); // try the suppress-exception variant ensure("bad launch returned non-NULL", ! LLLeap::create("empty", StringVec(), false)); } template<> template<> void object::test<4>() { set_test_name("bad launch"); // Synthesize bogus executable name std::string BADPYTHON(PYTHON.substr(0, PYTHON.length()-1) + "x"); CaptureLog log; std::string threw; try { LLLeap::create("bad exe", BADPYTHON); } CATCH_AND_STORE_WHAT_IN(threw, LLLeap::Error) ensure_contains("LLLeap::create() didn't throw", threw, "failed"); log.messageWith("failed"); log.messageWith(BADPYTHON); // try the suppress-exception variant ensure("bad launch returned non-NULL", ! LLLeap::create("bad exe", BADPYTHON, false)); } // Mimic a dummy little LLEventAPI that merely sends a reply back to its // requester on the "reply" pump. struct API { API(): mPump("API", true) { mPump.listen("API", boost::bind(&API::entry, this, _1)); } bool entry(const LLSD& request) { LLEventPumps::instance().obtain(request["reply"]).post("ack"); return false; } LLEventStream mPump; }; template<> template<> void object::test<5>() { set_test_name("round trip"); API api; NamedTempFile script("py", boost::lambda::_1 << "import re\n" "import sys\n" "from " << reader_module << " import get\n" // this will throw if the initial write to stdin // doesn't follow len:data protocol "initial = get()\n" "match = re.search(r\"'pump':'(.*?)'\", initial)\n" // this will throw if we couldn't find // 'pump':'etc.' in the initial write "reply = match.group(1)\n" "req = '''\\\n" "{'pump':'" << api.mPump.getName() << "','data':{'reply':'%s'}}\\\n" "''' % reply\n" // make a request on our little API "sys.stdout.write(':'.join((str(len(req)), req)))\n" "sys.stdout.flush()\n" // wait for its response "resp = get()\n" // it would be cleverer to be order-insensitive // about 'data' and 'pump'; hopefully the C++ // serializer doesn't change its rules soon "result = 'good' if (resp == \"{'data':'ack','pump':'%s'}\" % reply)\\\n" " else 'bad: ' + resp\n" // write 'good' or 'bad' to the log so we can observe "sys.stderr.write(result)\n"); CaptureLog log(LLError::LEVEL_INFO); waitfor(LLLeap::create(get_test_name(), sv(list_of(PYTHON)(script.getName())))); log.messageWith("good"); } } // namespace tut