/** * @file lleventdispatcher_test.cpp * @author Nat Goodspeed * @date 2011-01-20 * @brief Test for lleventdispatcher. * * $LicenseInfo:firstyear=2011&license=viewerlgpl$ * Copyright (c) 2011, Linden Research, Inc. * $/LicenseInfo$ */ // Precompiled header #include "linden_common.h" // associated header #include "lleventdispatcher.h" // STL headers // std headers // external library headers // other Linden headers #include "../test/lltut.h" #include "llsd.h" #include "llsdutil.h" #include "llevents.h" #include "stringize.h" #include "tests/wrapllerrs.h" #include "../test/catch_and_store_what_in.h" #include "../test/debug.h" #include #include #include #include #include #include #include #include #include using boost::lambda::constant; using boost::lambda::constant_ref; using boost::lambda::var; using namespace llsd; /***************************************************************************** * Example data, functions, classes *****************************************************************************/ // We don't need a whole lot of different arbitrary-params methods, just (no | // (const LLSD&) | arbitrary) args (function | static method | non-static // method), where 'arbitrary' is (every LLSD datatype + (const char*)). // But we need to register each one under different names for the different // registration styles. Don't forget LLEventDispatcher subclass methods(const // LLSD&). // However, the number of target parameter conversions we want to try exceeds // boost::fusion::invoke()'s supported parameter-list size. Break out two // different lists. #define NPARAMSa bool b, int i, float f, double d, const char* cp #define NPARAMSb const std::string& s, const LLUUID& uuid, const LLDate& date, \ const LLURI& uri, const std::vector& bin #define NARGSa b, i, f, d, cp #define NARGSb s, uuid, date, uri, bin // For some registration methods we need methods on a subclass of // LLEventDispatcher. To simplify things, we'll use this Dispatcher subclass // for all our testing, including testing its own methods. class Dispatcher: public LLEventDispatcher { public: Dispatcher(const std::string& name, const std::string& key): LLEventDispatcher(name, key) {} // sensing member, mutable because we want to know when we've reached our // const method too mutable LLSD llsd; void method1(const LLSD& obj) { llsd = obj; } void cmethod1(const LLSD& obj) const { llsd = obj; } }; // sensing vars, captured in a struct to make it convenient to clear them struct Vars { LLSD llsd; bool b; int i; float f; double d; // Capture param passed as char*. But merely storing a char* received from // our caller, possibly the .c_str() from a concatenation expression, // would be Bad: the pointer will be invalidated long before we can query // it. We could allocate a new chunk of memory, copy the string data and // point to that instead -- but hey, guess what, we already have a class // that does that! std::string cp; std::string s; LLUUID uuid; LLDate date; LLURI uri; std::vector bin; Vars(): // Only need to initialize the POD types, the rest should take care of // default-constructing themselves. b(false), i(0), f(0), d(0) {} // Detect any non-default values for convenient testing LLSD inspect() const { LLSD result; if (llsd.isDefined()) result["llsd"] = llsd; if (b) result["b"] = b; if (i) result["i"] = i; if (f) result["f"] = f; if (d) result["d"] = d; if (! cp.empty()) result["cp"] = cp; if (! s.empty()) result["s"] = s; if (uuid != LLUUID()) result["uuid"] = uuid; if (date != LLDate()) result["date"] = date; if (uri != LLURI()) result["uri"] = uri; if (! bin.empty()) result["bin"] = bin; return result; } /*------------- no-args (non-const, const, static) methods -------------*/ void method0() { debug()("method0()"); i = 17; } void cmethod0() const { debug()('c', NONL); const_cast(this)->method0(); } static void smethod0(); /*------------ Callable (non-const, const, static) methods -------------*/ void method1(const LLSD& obj) { debug()("method1(", obj, ")"); llsd = obj; } void cmethod1(const LLSD& obj) const { debug()('c', NONL); const_cast(this)->method1(obj); } static void smethod1(const LLSD& obj); /*-------- Arbitrary-params (non-const, const, static) methods ---------*/ void methodna(NPARAMSa) { // Because our const char* param cp might be NULL, and because we // intend to capture the value in a std::string, have to distinguish // between the NULL value and any non-NULL value. Use a convention // easy for a human reader: enclose any non-NULL value in single // quotes, reserving the unquoted string "NULL" to represent a NULL ptr. std::string vcp; if (cp == NULL) vcp = "NULL"; else vcp = std::string("'") + cp + "'"; debug()("methodna(", b, ", ", i, ", ", f, ", ", d, ", ", vcp, ")"); this->b = b; this->i = i; this->f = f; this->d = d; this->cp = vcp; } void methodnb(NPARAMSb) { std::ostringstream vbin; for (U8 byte: bin) { vbin << std::hex << std::setfill('0') << std::setw(2) << unsigned(byte); } debug()("methodnb(", "'", s, "'", ", ", uuid, ", ", date, ", '", uri, "'", ", ", vbin.str(), ")"); this->s = s; this->uuid = uuid; this->date = date; this->uri = uri; this->bin = bin; } void cmethodna(NPARAMSa) const { debug()('c', NONL); const_cast(this)->methodna(NARGSa); } void cmethodnb(NPARAMSb) const { debug()('c', NONL); const_cast(this)->methodnb(NARGSb); } static void smethodna(NPARAMSa); static void smethodnb(NPARAMSb); static Debug& debug() { // Lazily initialize this Debug instance so it can notice if main() // has forcibly set LOGTEST. If it were simply a static member, it // would already have examined the environment variable by the time // main() gets around to checking command-line switches. Since we have // a global static Vars instance, the same would be true of a plain // non-static member. static Debug sDebug("Vars"); return sDebug; } }; /*------- Global Vars instance for free functions and static methods -------*/ static Vars g; /*------------ Static Vars method implementations reference 'g' ------------*/ void Vars::smethod0() { debug()("smethod0() -> ", NONL); g.method0(); } void Vars::smethod1(const LLSD& obj) { debug()("smethod1(", obj, ") -> ", NONL); g.method1(obj); } void Vars::smethodna(NPARAMSa) { debug()("smethodna(...) -> ", NONL); g.methodna(NARGSa); } void Vars::smethodnb(NPARAMSb) { debug()("smethodnb(...) -> ", NONL); g.methodnb(NARGSb); } /*--------------------------- Reset global Vars ----------------------------*/ void clear() { g = Vars(); } /*------------------- Free functions also reference 'g' --------------------*/ void free0() { g.debug()("free0() -> ", NONL); g.method0(); } void free1(const LLSD& obj) { g.debug()("free1(", obj, ") -> ", NONL); g.method1(obj); } void freena(NPARAMSa) { g.debug()("freena(...) -> ", NONL); g.methodna(NARGSa); } void freenb(NPARAMSb) { g.debug()("freenb(...) -> ", NONL); g.methodnb(NARGSb); } /***************************************************************************** * TUT *****************************************************************************/ namespace tut { struct lleventdispatcher_data { Debug debug{"test"}; WrapLLErrs redirect; Dispatcher work; Vars v; std::string name, desc; // Capture our own copy of all registered functions' descriptions typedef std::map DescMap; DescMap descs; // Capture the Vars instance on which we expect each function to operate typedef std::map VarsMap; VarsMap funcvars; // Required structure for Callables with requirements LLSD required; // Parameter names for freena(), freenb() LLSD params; // Full, partial defaults arrays for params for freena(), freenb() LLSD dft_array_full, dft_array_partial; // Start index of partial defaults arrays const size_t partial_offset; // Full, partial defaults maps for params for freena(), freenb() LLSD dft_map_full, dft_map_partial; // Most of the above are indexed by "a" or "b". Useful to have an // array containing those strings for iterating. std::vector ab; lleventdispatcher_data(): work("test dispatcher", "op"), // map {d=double, array=[3 elements]} required(LLSDMap("d", LLSD::Real(0))("array", llsd::array(LLSD(), LLSD(), LLSD()))), // first several params are required, last couple optional partial_offset(3) { // This object is reconstructed for every test method. But // clear global variables every time too. ::clear(); const char* abs[] = { "a", "b" }; ab.assign(boost::begin(abs), boost::end(abs)); // Registration cases: // - (Callable | subclass const method | subclass non-const method | // non-subclass method) (with | without) required // - (Free function | static method | non-static method), (no | arbitrary) params, // array style // - (Free function | static method | non-static method), (no | arbitrary) params, // map style, (empty | partial | full) (array | map) defaults // - Map-style errors: // - (scalar | map) param names // - defaults scalar // - defaults array longer than params array // - defaults map with plural unknown param names // I hate to have to write things twice, because of having to keep // them consistent. If we had variadic functions, addf() would be // a variadic method, capturing the name and desc and passing them // plus "everything else" to work.add(). If I could return a pair // and use that pair as the first two args to work.add(), I'd do // that. But the best I can do with present C++ is to set two // instance variables as a side effect of addf(), and pass those // variables to each work.add() call. :-P /*------------------------- Callables --------------------------*/ // Arbitrary Callable with/out required params addf("free1", "free1", &g); work.add(name, desc, free1); addf("free1_req", "free1", &g); work.add(name, desc, free1, required); // Subclass non-const method with/out required params addf("Dmethod1", "method1", NULL); work.add(name, desc, &Dispatcher::method1); addf("Dmethod1_req", "method1", NULL); work.add(name, desc, &Dispatcher::method1, required); // Subclass const method with/out required params addf("Dcmethod1", "cmethod1", NULL); work.add(name, desc, &Dispatcher::cmethod1); addf("Dcmethod1_req", "cmethod1", NULL); work.add(name, desc, &Dispatcher::cmethod1, required); // Non-subclass method with/out required params addf("method1", "method1", &v); work.add(name, desc, boost::bind(&Vars::method1, boost::ref(v), _1)); addf("method1_req", "method1", &v); work.add(name, desc, boost::bind(&Vars::method1, boost::ref(v), _1), required); /*--------------- Arbitrary params, array style ----------------*/ // (Free function | static method) with (no | arbitrary) params, array style addf("free0_array", "free0", &g); work.add(name, desc, free0); addf("freena_array", "freena", &g); work.add(name, desc, freena); addf("freenb_array", "freenb", &g); work.add(name, desc, freenb); addf("smethod0_array", "smethod0", &g); work.add(name, desc, &Vars::smethod0); addf("smethodna_array", "smethodna", &g); work.add(name, desc, &Vars::smethodna); addf("smethodnb_array", "smethodnb", &g); work.add(name, desc, &Vars::smethodnb); // Non-static method with (no | arbitrary) params, array style addf("method0_array", "method0", &v); work.add(name, desc, &Vars::method0, boost::lambda::var(v)); addf("methodna_array", "methodna", &v); work.add(name, desc, &Vars::methodna, boost::lambda::var(v)); addf("methodnb_array", "methodnb", &v); work.add(name, desc, &Vars::methodnb, boost::lambda::var(v)); /*---------------- Arbitrary params, map style -----------------*/ // We lay out each params list as an array, also each array of // default values we'll register. We'll zip these into // (param=value) maps. Why not define them as maps and just // extract the keys and values to arrays? Because that wouldn't // give us the right params-list order. // freena(), methodna(), cmethodna(), smethodna() all take same param list. // Same for freenb() et al. params = LLSDMap("a", llsd::array("b", "i", "f", "d", "cp")) ("b", llsd::array("s", "uuid", "date", "uri", "bin")); debug("params:\n", params, "\n" "params[\"a\"]:\n", params["a"], "\n" "params[\"b\"]:\n", params["b"]); // default LLSD::Binary value std::vector binary; for (size_t ix = 0, h = 0xaa; ix < 6; ++ix, h += 0x11) { binary.push_back((U8)h); } // Full defaults arrays. We actually don't care what the LLUUID or // LLDate values are, as long as they're different from the // LLUUID() and LLDate() default values so inspect() will report // them. dft_array_full = LLSDMap("a", llsd::array(true, 17, 3.14, 123456.78, "classic")) ("b", llsd::array("string", LLUUID::generateNewID(), LLDate::now(), LLURI("http://www.ietf.org/rfc/rfc3986.txt"), binary)); debug("dft_array_full:\n", dft_array_full); // Partial defaults arrays. for (LLSD::String a: ab) { LLSD::Integer partition(std::min(partial_offset, dft_array_full[a].size())); dft_array_partial[a] = llsd_copy_array(dft_array_full[a].beginArray() + partition, dft_array_full[a].endArray()); } debug("dft_array_partial:\n", dft_array_partial); for(LLSD::String a: ab) { // Generate full defaults maps by zipping (params, dft_array_full). dft_map_full[a] = zipmap(params[a], dft_array_full[a]); // Generate partial defaults map by zipping alternate entries from // (params, dft_array_full). Part of the point of using map-style // defaults is to allow any subset of the target function's // parameters to be optional, not just the rightmost. for (LLSD::Integer ix = 0, ixend = params[a].size(); ix < ixend; ix += 2) { dft_map_partial[a][params[a][ix].asString()] = dft_array_full[a][ix]; } } debug("dft_map_full:\n", dft_map_full, "\n" "dft_map_partial:\n", dft_map_partial); // (Free function | static method) with (no | arbitrary) params, // map style, no (empty array) defaults addf("free0_map", "free0", &g); work.add(name, desc, free0, LLSD::emptyArray()); addf("smethod0_map", "smethod0", &g); work.add(name, desc, &Vars::smethod0, LLSD::emptyArray()); addf("freena_map_allreq", "freena", &g); work.add(name, desc, freena, params["a"]); addf("freenb_map_allreq", "freenb", &g); work.add(name, desc, freenb, params["b"]); addf("smethodna_map_allreq", "smethodna", &g); work.add(name, desc, &Vars::smethodna, params["a"]); addf("smethodnb_map_allreq", "smethodnb", &g); work.add(name, desc, &Vars::smethodnb, params["b"]); // Non-static method with (no | arbitrary) params, map style, no // (empty array) defaults addf("method0_map", "method0", &v); work.add(name, desc, &Vars::method0, var(v), LLSD::emptyArray()); addf("methodna_map_allreq", "methodna", &v); work.add(name, desc, &Vars::methodna, var(v), params["a"]); addf("methodnb_map_allreq", "methodnb", &v); work.add(name, desc, &Vars::methodnb, var(v), params["b"]); // Except for the "more (array | map) defaults than params" error // cases, tested separately below, the (partial | full)(array | // map) defaults cases don't apply to no-params functions/methods. // So eliminate free0, smethod0, method0 from the cases below. // (Free function | static method) with arbitrary params, map // style, partial (array | map) defaults addf("freena_map_leftreq", "freena", &g); work.add(name, desc, freena, params["a"], dft_array_partial["a"]); addf("freenb_map_leftreq", "freenb", &g); work.add(name, desc, freenb, params["b"], dft_array_partial["b"]); addf("smethodna_map_leftreq", "smethodna", &g); work.add(name, desc, &Vars::smethodna, params["a"], dft_array_partial["a"]); addf("smethodnb_map_leftreq", "smethodnb", &g); work.add(name, desc, &Vars::smethodnb, params["b"], dft_array_partial["b"]); addf("freena_map_skipreq", "freena", &g); work.add(name, desc, freena, params["a"], dft_map_partial["a"]); addf("freenb_map_skipreq", "freenb", &g); work.add(name, desc, freenb, params["b"], dft_map_partial["b"]); addf("smethodna_map_skipreq", "smethodna", &g); work.add(name, desc, &Vars::smethodna, params["a"], dft_map_partial["a"]); addf("smethodnb_map_skipreq", "smethodnb", &g); work.add(name, desc, &Vars::smethodnb, params["b"], dft_map_partial["b"]); // Non-static method with arbitrary params, map style, partial // (array | map) defaults addf("methodna_map_leftreq", "methodna", &v); work.add(name, desc, &Vars::methodna, var(v), params["a"], dft_array_partial["a"]); addf("methodnb_map_leftreq", "methodnb", &v); work.add(name, desc, &Vars::methodnb, var(v), params["b"], dft_array_partial["b"]); addf("methodna_map_skipreq", "methodna", &v); work.add(name, desc, &Vars::methodna, var(v), params["a"], dft_map_partial["a"]); addf("methodnb_map_skipreq", "methodnb", &v); work.add(name, desc, &Vars::methodnb, var(v), params["b"], dft_map_partial["b"]); // (Free function | static method) with arbitrary params, map // style, full (array | map) defaults addf("freena_map_adft", "freena", &g); work.add(name, desc, freena, params["a"], dft_array_full["a"]); addf("freenb_map_adft", "freenb", &g); work.add(name, desc, freenb, params["b"], dft_array_full["b"]); addf("smethodna_map_adft", "smethodna", &g); work.add(name, desc, &Vars::smethodna, params["a"], dft_array_full["a"]); addf("smethodnb_map_adft", "smethodnb", &g); work.add(name, desc, &Vars::smethodnb, params["b"], dft_array_full["b"]); addf("freena_map_mdft", "freena", &g); work.add(name, desc, freena, params["a"], dft_map_full["a"]); addf("freenb_map_mdft", "freenb", &g); work.add(name, desc, freenb, params["b"], dft_map_full["b"]); addf("smethodna_map_mdft", "smethodna", &g); work.add(name, desc, &Vars::smethodna, params["a"], dft_map_full["a"]); addf("smethodnb_map_mdft", "smethodnb", &g); work.add(name, desc, &Vars::smethodnb, params["b"], dft_map_full["b"]); // Non-static method with arbitrary params, map style, full // (array | map) defaults addf("methodna_map_adft", "methodna", &v); work.add(name, desc, &Vars::methodna, var(v), params["a"], dft_array_full["a"]); addf("methodnb_map_adft", "methodnb", &v); work.add(name, desc, &Vars::methodnb, var(v), params["b"], dft_array_full["b"]); addf("methodna_map_mdft", "methodna", &v); work.add(name, desc, &Vars::methodna, var(v), params["a"], dft_map_full["a"]); addf("methodnb_map_mdft", "methodnb", &v); work.add(name, desc, &Vars::methodnb, var(v), params["b"], dft_map_full["b"]); // All the above are expected to succeed, and are setup for the // tests to follow. Registration error cases are exercised as // tests rather than as test setup. } void addf(const std::string& n, const std::string& d, Vars* v) { // This method is to capture in our own DescMap the name and // description of every registered function, for metadata query // testing. descs[n] = d; // Also capture the Vars instance on which each function should operate. funcvars[n] = v; // See constructor for rationale for setting these instance vars. this->name = n; this->desc = d; } void verify_descs() { // Copy descs to a temp map of same type. DescMap forgotten(descs.begin(), descs.end()); for (LLEventDispatcher::NameDesc nd: work) { DescMap::iterator found = forgotten.find(nd.first); ensure(stringize("LLEventDispatcher records function '", nd.first, "' we didn't enter"), found != forgotten.end()); ensure_equals(stringize("LLEventDispatcher desc '", nd.second, "' doesn't match what we entered: '", found->second, "'"), nd.second, found->second); // found in our map the name from LLEventDispatcher, good, erase // our map entry forgotten.erase(found); } if (! forgotten.empty()) { std::ostringstream out; out << "LLEventDispatcher failed to report"; const char* delim = ": "; for (const DescMap::value_type& fme: forgotten) { out << delim << fme.first; delim = ", "; } throw failure(out.str()); } } Vars* varsfor(const std::string& name) { VarsMap::const_iterator found = funcvars.find(name); ensure(stringize("No Vars* for ", name), found != funcvars.end()); ensure(stringize("NULL Vars* for ", name), found->second); return found->second; } void ensure_has(const std::string& outer, const std::string& inner) { ensure(stringize("'", outer, "' does not contain '", inner, "'").c_str(), outer.find(inner) != std::string::npos); } std::string call_exc(const std::string& func, const LLSD& args, const std::string& exc_frag) { // This method was written when LLEventDispatcher responded to // name or argument errors with LL_ERRS, hence the name: we used // to have to intercept LL_ERRS by making it throw. Now we set up // to catch an error response instead. But -- for that we need to // be able to sneak a "reply" key into args, which must be a Map. if (! (args.isUndefined() or args.isMap())) fail(stringize("can't test call_exc() with ", args)); LLEventStream replypump("reply"); LLSD reply; LLTempBoundListener bound{ replypump.listen( "listener", [&reply](const LLSD& event) { reply = event; return false; }) }; LLSD modargs{ args }; modargs["reply"] = replypump.getName(); if (func.empty()) { work(modargs); } else { work(func, modargs); } ensure("no error response", reply.has("error")); ensure_has(reply["error"], exc_frag); return reply["error"]; } void call_logerr(const std::string& func, const LLSD& args, const std::string& frag) { CaptureLog capture; work(func, args); capture.messageWith(frag); } LLSD getMetadata(const std::string& name) { LLSD meta(work.getMetadata(name)); ensure(stringize("No metadata for ", name), meta.isDefined()); return meta; } // From two related LLSD arrays, e.g. a param-names array and a values // array, zip them together into an LLSD map. LLSD zipmap(const LLSD& keys, const LLSD& values) { LLSD map; for (LLSD::Integer i = 0, iend = keys.size(); i < iend; ++i) { // Have to select asString() since you can index an LLSD // object with either String or Integer. map[keys[i].asString()] = values[i]; } return map; } // If I call this ensure_equals(), it blocks visibility of all other // ensure_equals() overloads. Normally I could say 'using // baseclass::ensure_equals;' and fix that, but I don't know what the // base class is! void ensure_llsd(const std::string& msg, const LLSD& actual, const LLSD& expected, U32 bits) { std::ostringstream out; if (! msg.empty()) { out << msg << ": "; } out << "expected " << expected << ", actual " << actual; ensure(out.str(), llsd_equals(actual, expected, bits)); } void ensure_llsd(const LLSD& actual, const LLSD& expected, U32 bits) { ensure_llsd("", actual, expected, bits); } }; typedef test_group lleventdispatcher_group; typedef lleventdispatcher_group::object object; lleventdispatcher_group lleventdispatchergrp("lleventdispatcher"); // Call cases: // - (try_call | call) (explicit name | event key) (real | bogus) name // - Callable with args that (do | do not) match required // - (Free function | non-static method), no args, (array | map) style // - (Free function | non-static method), arbitrary args, // (array style with (scalar | map) | map style with scalar) // - (Free function | non-static method), arbitrary args, array style with // array (too short | too long | just right) // [trap LL_WARNS for too-long case?] // - (Free function | non-static method), arbitrary args, map style with // (array | map) (all | too many | holes (with | without) defaults) // - const char* param gets ("" | NULL) // Query cases: // - Iterate over all (with | without) remove() // - getDispatchKey() // - Callable style (with | without) required // - (Free function | non-static method), array style, (no | arbitrary) params // - (Free function | non-static method), map style, (no | arbitrary) params, // (empty | full | partial (array | map)) defaults template<> template<> void object::test<1>() { set_test_name("map-style registration with non-array params"); // Pass "param names" as scalar or as map LLSD attempts(llsd::array(17, LLSDMap("pi", 3.14)("two", 2))); foreach(LLSD ae, inArray(attempts)) { std::string threw = catch_what([this, &ae](){ work.add("freena_err", "freena", freena, ae); }); ensure_has(threw, "must be an array"); } } template<> template<> void object::test<2>() { set_test_name("map-style registration with badly-formed defaults"); std::string threw = catch_what([this](){ work.add("freena_err", "freena", freena, llsd::array("a", "b"), 17); }); ensure_has(threw, "must be a map or an array"); } template<> template<> void object::test<3>() { set_test_name("map-style registration with too many array defaults"); std::string threw = catch_what([this](){ work.add("freena_err", "freena", freena, llsd::array("a", "b"), llsd::array(17, 0.9, "gack")); }); ensure_has(threw, "shorter than"); } template<> template<> void object::test<4>() { set_test_name("map-style registration with too many map defaults"); std::string threw = catch_what([this](){ work.add("freena_err", "freena", freena, llsd::array("a", "b"), LLSDMap("b", 17)("foo", 3.14)("bar", "sinister")); }); ensure_has(threw, "nonexistent params"); ensure_has(threw, "foo"); ensure_has(threw, "bar"); } template<> template<> void object::test<5>() { set_test_name("query all"); verify_descs(); } template<> template<> void object::test<6>() { set_test_name("query all with remove()"); ensure("remove('bogus') returned true", ! work.remove("bogus")); ensure("remove('real') returned false", work.remove("free1")); // Of course, remove that from 'descs' too... descs.erase("free1"); verify_descs(); } template<> template<> void object::test<7>() { set_test_name("getDispatchKey()"); ensure_equals(work.getDispatchKey(), "op"); } template<> template<> void object::test<8>() { set_test_name("query Callables with/out required params"); LLSD names(llsd::array("free1", "Dmethod1", "Dcmethod1", "method1")); foreach(LLSD nm, inArray(names)) { LLSD metadata(getMetadata(nm)); ensure_equals("name mismatch", metadata["name"], nm); ensure_equals(metadata["desc"].asString(), descs[nm]); ensure("should not have required structure", metadata["required"].isUndefined()); ensure("should not have optional", metadata["optional"].isUndefined()); std::string name_req(nm.asString() + "_req"); metadata = getMetadata(name_req); ensure_equals(metadata["name"].asString(), name_req); ensure_equals(metadata["desc"].asString(), descs[name_req]); ensure_equals("required mismatch", required, metadata["required"]); ensure("should not have optional", metadata["optional"].isUndefined()); } } template<> template<> void object::test<9>() { set_test_name("query array-style functions/methods"); // Associate each registered name with expected arity. LLSD expected(llsd::array (llsd::array (0, llsd::array("free0_array", "smethod0_array", "method0_array")), llsd::array (5, llsd::array("freena_array", "smethodna_array", "methodna_array")), llsd::array (5, llsd::array("freenb_array", "smethodnb_array", "methodnb_array")))); foreach(LLSD ae, inArray(expected)) { LLSD::Integer arity(ae[0].asInteger()); LLSD names(ae[1]); LLSD req(LLSD::emptyArray()); if (arity) req[arity - 1] = LLSD(); for (LLSD nm: inArray(names)) { LLSD metadata(getMetadata(nm)); ensure_equals("name mismatch", metadata["name"], nm); ensure_equals(metadata["desc"].asString(), descs[nm]); ensure_equals(stringize("mismatched required for ", nm.asString()), metadata["required"], req); ensure("should not have optional", metadata["optional"].isUndefined()); } } } template<> template<> void object::test<10>() { set_test_name("query map-style no-params functions/methods"); // - (Free function | non-static method), map style, no params (ergo // no defaults) LLSD names(llsd::array("free0_map", "smethod0_map", "method0_map")); foreach(LLSD nm, inArray(names)) { LLSD metadata(getMetadata(nm)); ensure_equals("name mismatch", metadata["name"], nm); ensure_equals(metadata["desc"].asString(), descs[nm]); ensure("should not have required", (metadata["required"].isUndefined() || metadata["required"].size() == 0)); ensure("should not have optional", metadata["optional"].isUndefined()); } } template<> template<> void object::test<11>() { set_test_name("query map-style arbitrary-params functions/methods: " "full array defaults vs. full map defaults"); // With functions registered with no defaults ("_allreq" suffixes), // there is of course no difference between array defaults and map // defaults. (We don't even bother registering with LLSD::emptyArray() // vs. LLSD::emptyMap().) With functions registered with all defaults, // there should (!) be no difference beween array defaults and map // defaults. Verify, so we can ignore the distinction for all other // tests. LLSD equivalences(llsd::array (llsd::array("freena_map_adft", "freena_map_mdft"), llsd::array("freenb_map_adft", "freenb_map_mdft"), llsd::array("smethodna_map_adft", "smethodna_map_mdft"), llsd::array("smethodnb_map_adft", "smethodnb_map_mdft"), llsd::array("methodna_map_adft", "methodna_map_mdft"), llsd::array("methodnb_map_adft", "methodnb_map_mdft"))); foreach(LLSD eq, inArray(equivalences)) { LLSD adft(eq[0]); LLSD mdft(eq[1]); // We can't just compare the results of the two getMetadata() // calls, because they contain ["name"], which are different. So // capture them, verify that each ["name"] is as expected, then // remove for comparing the rest. LLSD ameta(getMetadata(adft)); LLSD mmeta(getMetadata(mdft)); ensure_equals("adft name", adft, ameta["name"]); ensure_equals("mdft name", mdft, mmeta["name"]); ameta.erase("name"); mmeta.erase("name"); ensure_equals(stringize("metadata for ", adft.asString(), " vs. ", mdft.asString()), ameta, mmeta); } } template<> template<> void object::test<12>() { set_test_name("query map-style arbitrary-params functions/methods"); // - (Free function | non-static method), map style, arbitrary params, // (empty | full | partial (array | map)) defaults // Generate maps containing all parameter names for cases in which all // params are required. Also maps containing left requirements for // partial defaults arrays. Also defaults maps from defaults arrays. LLSD allreq, leftreq, rightdft; for (LLSD::String a: ab) { // The map in which all params are required uses params[a] as // keys, with all isUndefined() as values. We can accomplish that // by passing zipmap() an empty values array. allreq[a] = zipmap(params[a], LLSD::emptyArray()); // Same for leftreq, save that we use the subset of the params not // supplied by dft_array_partial[a]. LLSD::Integer partition(params[a].size() - dft_array_partial[a].size()); leftreq[a] = zipmap(llsd_copy_array(params[a].beginArray(), params[a].beginArray() + partition), LLSD::emptyArray()); // Generate map pairing dft_array_partial[a] values with their // param names. rightdft[a] = zipmap(llsd_copy_array(params[a].beginArray() + partition, params[a].endArray()), dft_array_partial[a]); } debug("allreq:\n", allreq, "\n" "leftreq:\n", leftreq, "\n" "rightdft:\n", rightdft); // Generate maps containing parameter names not provided by the // dft_map_partial maps. LLSD skipreq(allreq); for (LLSD::String a: ab) { for (const MapEntry& me: inMap(dft_map_partial[a])) { skipreq[a].erase(me.first); } } debug("skipreq:\n", skipreq); LLSD groups(llsd::array // array of groups (llsd::array // group (llsd::array("freena_map_allreq", "smethodna_map_allreq", "methodna_map_allreq"), llsd::array(allreq["a"], LLSD())), // required, optional llsd::array // group (llsd::array("freenb_map_allreq", "smethodnb_map_allreq", "methodnb_map_allreq"), llsd::array(allreq["b"], LLSD())), // required, optional llsd::array // group (llsd::array("freena_map_leftreq", "smethodna_map_leftreq", "methodna_map_leftreq"), llsd::array(leftreq["a"], rightdft["a"])), // required, optional llsd::array // group (llsd::array("freenb_map_leftreq", "smethodnb_map_leftreq", "methodnb_map_leftreq"), llsd::array(leftreq["b"], rightdft["b"])), // required, optional llsd::array // group (llsd::array("freena_map_skipreq", "smethodna_map_skipreq", "methodna_map_skipreq"), llsd::array(skipreq["a"], dft_map_partial["a"])), // required, optional llsd::array // group (llsd::array("freenb_map_skipreq", "smethodnb_map_skipreq", "methodnb_map_skipreq"), llsd::array(skipreq["b"], dft_map_partial["b"])), // required, optional // We only need mention the full-map-defaults ("_mdft" suffix) // registrations, having established their equivalence with the // full-array-defaults ("_adft" suffix) registrations in another test. llsd::array // group (llsd::array("freena_map_mdft", "smethodna_map_mdft", "methodna_map_mdft"), llsd::array(LLSD::emptyMap(), dft_map_full["a"])), // required, optional llsd::array // group (llsd::array("freenb_map_mdft", "smethodnb_map_mdft", "methodnb_map_mdft"), llsd::array(LLSD::emptyMap(), dft_map_full["b"])))); // required, optional for (LLSD grp: inArray(groups)) { // Internal structure of each group in 'groups': LLSD names(grp[0]); LLSD required(grp[1][0]); LLSD optional(grp[1][1]); debug("For ", names, ",\n", "required:\n", required, "\n" "optional:\n", optional); // Loop through 'names' for (LLSD nm: inArray(names)) { LLSD metadata(getMetadata(nm)); ensure_equals("name mismatch", metadata["name"], nm); ensure_equals(nm.asString(), metadata["desc"].asString(), descs[nm]); ensure_equals(stringize(nm, " required mismatch"), metadata["required"], required); ensure_equals(stringize(nm, " optional mismatch"), metadata["optional"], optional); } } } template<> template<> void object::test<13>() { set_test_name("try_call()"); ensure("try_call(bogus name, LLSD()) returned true", ! work.try_call("freek", LLSD())); ensure("try_call(bogus name) returned true", ! work.try_call(LLSDMap("op", "freek"))); ensure("try_call(real name, LLSD()) returned false", work.try_call("free0_array", LLSD())); ensure("try_call(real name) returned false", work.try_call(LLSDMap("op", "free0_map"))); } template<> template<> void object::test<14>() { set_test_name("call with bad name"); call_exc("freek", LLSD(), "not found"); std::string threw = call_exc("", LLSDMap("op", "freek"), "bad"); ensure_has(threw, "op"); ensure_has(threw, "freek"); } template<> template<> void object::test<15>() { set_test_name("call with event key"); // We don't need a separate test for operator()(string, LLSD) with // valid name, because all the rest of the tests exercise that case. // The one we don't exercise elsewhere is operator()(LLSD) with valid // name, so here it is. work(LLSDMap("op", "free0_map")); ensure_equals(g.i, 17); } // Cannot be defined inside function body... remind me again why we use C++... :-P struct CallablesTriple { std::string name, name_req; LLSD& llsd; }; template<> template<> void object::test<16>() { set_test_name("call Callables"); CallablesTriple tests[] = { { "free1", "free1_req", g.llsd }, { "Dmethod1", "Dmethod1_req", work.llsd }, { "Dcmethod1", "Dcmethod1_req", work.llsd }, { "method1", "method1_req", v.llsd } }; // Arbitrary LLSD value that we should be able to pass to Callables // without 'required', but should not be able to pass to Callables // with 'required'. LLSD answer(42); // LLSD value matching 'required' according to llsd_matches() rules. LLSD matching(LLSDMap("d", 3.14)("array", llsd::array("answer", true, answer))); // Okay, walk through 'tests'. for (const CallablesTriple& tr: tests) { // Should be able to pass 'answer' to Callables registered // without 'required'. work(tr.name, answer); ensure_equals("answer mismatch", tr.llsd, answer); // Should NOT be able to pass 'answer' to Callables registered // with 'required'. call_logerr(tr.name_req, answer, "bad request"); // But SHOULD be able to pass 'matching' to Callables registered // with 'required'. work(tr.name_req, matching); ensure_equals("matching mismatch", tr.llsd, matching); } } template<> template<> void object::test<17>() { set_test_name("passing wrong args to (map | array)-style registrations"); // Pass scalar/map to array-style functions, scalar/array to map-style // functions. It seems pointless to repeat this with every variation: // (free function | non-static method), (no | arbitrary) args. We // should only need to engage it for one map-style registration and // one array-style registration. // Now that LLEventDispatcher has been extended to treat an LLSD // scalar as a single-entry array, the error we expect in this case is // that apply() is trying to pass that non-empty array to a nullary // function. call_logerr("free0_array", 17, "LL::apply"); // similarly, apply() doesn't accept an LLSD Map call_logerr("free0_array", LLSDMap("pi", 3.14), "unsupported"); std::string map_exc("needs a map"); call_logerr("free0_map", 17, map_exc); // Passing an array to a map-style function works now! No longer an // error case! // call_exc("free0_map", llsd::array("a", "b"), map_exc); } template<> template<> void object::test<18>() { set_test_name("call no-args functions"); LLSD names(llsd::array ("free0_array", "free0_map", "smethod0_array", "smethod0_map", "method0_array", "method0_map")); foreach(LLSD name, inArray(names)) { // Look up the Vars instance for this function. Vars* vars(varsfor(name)); // Both the global and stack Vars instances are automatically // cleared at the start of each test method. But since we're // calling these things several different times in the same // test method, manually reset the Vars between each. *vars = Vars(); ensure_equals(vars->i, 0); // call function with empty array (or LLSD(), should be equivalent) work(name, LLSD()); ensure_equals(vars->i, 17); } } // Break out this data because we use it in a couple different tests. LLSD array_funcs(llsd::array (LLSDMap("a", "freena_array") ("b", "freenb_array"), LLSDMap("a", "smethodna_array")("b", "smethodnb_array"), LLSDMap("a", "methodna_array") ("b", "methodnb_array"))); template<> template<> void object::test<19>() { set_test_name("call array-style functions with wrong-length arrays"); // Could have different wrong-length arrays for *na and for *nb, but // since they both take 5 params... LLSD tooshort(llsd::array("this", "array", "too", "short")); LLSD toolong (llsd::array("this", "array", "is", "one", "too", "long")); LLSD badargs (llsd::array(tooshort, toolong)); for (const LLSD& toosomething: inArray(badargs)) { for (const LLSD& funcsab: inArray(array_funcs)) { for (const llsd::MapEntry& e: inMap(funcsab)) { // apply() complains about wrong number of array entries call_logerr(e.second, toosomething, "LL::apply"); } } } } template<> template<> void object::test<20>() { set_test_name("call array-style functions with (just right | too long) arrays"); std::vector binary; for (size_t h(0x01), i(0); i < 5; h+= 0x22, ++i) { binary.push_back((U8)h); } LLSD args(LLSDMap("a", llsd::array(true, 17, 3.14, 123.456, "char*")) ("b", llsd::array("string", LLUUID("01234567-89ab-cdef-0123-456789abcdef"), LLDate("2011-02-03T15:07:00Z"), LLURI("http://secondlife.com"), binary))); LLSD expect; for (LLSD::String a: ab) { expect[a] = zipmap(params[a], args[a]); } // Adjust expect["a"]["cp"] for special Vars::cp treatment. expect["a"]["cp"] = stringize("'", expect["a"]["cp"].asString(), "'"); debug("expect: ", expect); for (const LLSD& funcsab: inArray(array_funcs)) { for (LLSD::String a: ab) { // Reset the Vars instance before each call Vars* vars(varsfor(funcsab[a])); *vars = Vars(); work(funcsab[a], args[a]); ensure_llsd(stringize(funcsab[a].asString(), ": expect[\"", a, "\"] mismatch"), vars->inspect(), expect[a], 7); // 7 bits ~= 2 decimal digits } } } template<> template<> void object::test<21>() { set_test_name("verify that passing LLSD() to const char* sends NULL"); ensure_equals("Vars::cp init", v.cp, ""); work("methodna_map_mdft", LLSDMap("cp", LLSD())); ensure_equals("passing LLSD()", v.cp, "NULL"); work("methodna_map_mdft", LLSDMap("cp", "")); ensure_equals("passing \"\"", v.cp, "''"); work("methodna_map_mdft", LLSDMap("cp", "non-NULL")); ensure_equals("passing \"non-NULL\"", v.cp, "'non-NULL'"); } template<> template<> void object::test<22>() { set_test_name("call map-style functions with (full | oversized) (arrays | maps)"); const char binary[] = "\x99\x88\x77\x66\x55"; LLSD array_full(LLSDMap ("a", llsd::array(false, 255, 98.6, 1024.5, "pointer")) ("b", llsd::array("object", LLUUID::generateNewID(), LLDate::now(), LLURI("http://wiki.lindenlab.com/wiki"), LLSD::Binary(boost::begin(binary), boost::end(binary))))); LLSD array_overfull(array_full); for (LLSD::String a: ab) { array_overfull[a].append("bogus"); } debug("array_full: ", array_full, "\n" "array_overfull: ", array_overfull); // We rather hope that LLDate::now() will generate a timestamp // distinct from the one it generated in the constructor, moments ago. ensure_not_equals("Timestamps too close", array_full["b"][2].asDate(), dft_array_full["b"][2].asDate()); // We /insist/ that LLUUID::generateNewID() do so. ensure_not_equals("UUID collision", array_full["b"][1].asUUID(), dft_array_full["b"][1].asUUID()); LLSD map_full, map_overfull; for (LLSD::String a: ab) { map_full[a] = zipmap(params[a], array_full[a]); map_overfull[a] = map_full[a]; map_overfull[a]["extra"] = "ignore"; } debug("map_full: ", map_full, "\n" "map_overfull: ", map_overfull); LLSD expect(map_full); // Twiddle the const char* param. expect["a"]["cp"] = std::string("'") + expect["a"]["cp"].asString() + "'"; // Another adjustment. For each data type, we're trying to distinguish // three values: the Vars member's initial value (member wasn't // stored; control never reached the set function), the registered // default param value from dft_array_full, and the array_full value // in this test. But bool can only distinguish two values. In this // case, we want to differentiate the local array_full value from the // dft_array_full value, so we use 'false'. However, that means // Vars::inspect() doesn't differentiate it from the initial value, // so won't bother returning it. Predict that behavior to match the // LLSD values. expect["a"].erase("b"); debug("expect: ", expect); // For this test, calling functions registered with different sets of // parameter defaults should make NO DIFFERENCE WHATSOEVER. Every call // should pass all params. LLSD names(LLSDMap ("a", llsd::array ("freena_map_allreq", "smethodna_map_allreq", "methodna_map_allreq", "freena_map_leftreq", "smethodna_map_leftreq", "methodna_map_leftreq", "freena_map_skipreq", "smethodna_map_skipreq", "methodna_map_skipreq", "freena_map_adft", "smethodna_map_adft", "methodna_map_adft", "freena_map_mdft", "smethodna_map_mdft", "methodna_map_mdft")) ("b", llsd::array ("freenb_map_allreq", "smethodnb_map_allreq", "methodnb_map_allreq", "freenb_map_leftreq", "smethodnb_map_leftreq", "methodnb_map_leftreq", "freenb_map_skipreq", "smethodnb_map_skipreq", "methodnb_map_skipreq", "freenb_map_adft", "smethodnb_map_adft", "methodnb_map_adft", "freenb_map_mdft", "smethodnb_map_mdft", "methodnb_map_mdft"))); // Treat (full | overfull) (array | map) the same. LLSD argssets(llsd::array(array_full, array_overfull, map_full, map_overfull)); foreach(const LLSD& args, inArray(argssets)) { for (LLSD::String a: ab) { for (LLSD::String name: inArray(names[a])) { // Reset the Vars instance Vars* vars(varsfor(name)); *vars = Vars(); work(name, args[a]); ensure_llsd(stringize(name, ": expect[\"", a, "\"] mismatch"), vars->inspect(), expect[a], 7); // 7 bits, 2 decimal digits // intercept LL_WARNS for the two overfull cases? } } } } } // namespace tut