summaryrefslogtreecommitdiff
path: root/indra/newview
diff options
context:
space:
mode:
Diffstat (limited to 'indra/newview')
-rw-r--r--indra/newview/llinventorylistener.cpp188
-rw-r--r--indra/newview/llinventorylistener.h9
-rw-r--r--indra/newview/scripts/lua/require/LLInventory.lua41
-rw-r--r--indra/newview/scripts/lua/require/UI.lua5
-rw-r--r--indra/newview/scripts/lua/require/result_view.lua98
-rw-r--r--indra/newview/scripts/lua/test_LLInventory.lua15
-rw-r--r--indra/newview/scripts/lua/test_inv_resultset.lua18
-rw-r--r--indra/newview/scripts/lua/test_result_view.lua55
-rw-r--r--indra/newview/scripts/lua/test_setdtor.lua91
-rw-r--r--indra/newview/tests/llluamanager_test.cpp4
10 files changed, 455 insertions, 69 deletions
diff --git a/indra/newview/llinventorylistener.cpp b/indra/newview/llinventorylistener.cpp
index 753ad3ddeb..79726c3e0b 100644
--- a/indra/newview/llinventorylistener.cpp
+++ b/indra/newview/llinventorylistener.cpp
@@ -31,7 +31,9 @@
#include "llinventoryfunctions.h"
#include "lltransutil.h"
#include "llwearableitemslist.h"
+#include "resultset.h"
#include "stringize.h"
+#include <algorithm> // std::min()
constexpr S32 MAX_ITEM_LIMIT = 100;
@@ -41,7 +43,7 @@ LLInventoryListener::LLInventoryListener()
{
add("getItemsInfo",
"Return information about items or folders defined in [\"item_ids\"]:\n"
- "reply will contain [\"items\"] and [\"categories\"] tables accordingly",
+ "reply will contain [\"items\"] and [\"categories\"] result set keys",
&LLInventoryListener::getItemsInfo,
llsd::map("item_ids", LLSD(), "reply", LLSD()));
@@ -61,78 +63,115 @@ LLInventoryListener::LLInventoryListener()
&LLInventoryListener::getBasicFolderID,
llsd::map("ft_name", LLSD(), "reply", LLSD()));
- add("getDirectDescendents",
- "Return the direct descendents(both items and folders) of the [\"folder_id\"]",
- &LLInventoryListener::getDirectDescendents,
+ add("getDirectDescendants",
+ "Return result set keys [\"categories\"] and [\"items\"] for the direct\n"
+ "descendants of the [\"folder_id\"]",
+ &LLInventoryListener::getDirectDescendants,
llsd::map("folder_id", LLSD(), "reply", LLSD()));
- add("collectDescendentsIf",
- "Return the descendents(both items and folders) of the [\"folder_id\"], if it passes specified filters:\n"
+ add("collectDescendantsIf",
+ "Return result set keys [\"categories\"] and [\"items\"] for the descendants\n"
+ "of the [\"folder_id\"], if it passes specified filters:\n"
"[\"name\"] is a substring of object's name,\n"
"[\"desc\"] is a substring of object's description,\n"
"asset [\"type\"] corresponds to the string name of the object's asset type\n"
- "[\"limit\"] sets item count limit in reply, maximum and default is 100\n"
+ "[\"limit\"] sets item count limit in result set (default unlimited)\n"
"[\"filter_links\"]: EXCLUDE_LINKS - don't show links, ONLY_LINKS - only show links, INCLUDE_LINKS - show links too (default)",
- &LLInventoryListener::collectDescendentsIf,
+ &LLInventoryListener::collectDescendantsIf,
llsd::map("folder_id", LLSD(), "reply", LLSD()));
- }
-
-void add_item_info(LLEventAPI::Response& response, LLViewerInventoryItem* item)
-{
- response["items"].insert(item->getUUID().asString(),
- llsd::map("name", item->getName(),
- "parent_id", item->getParentUUID(),
- "desc", item->getDescription(),
- "inv_type", LLInventoryType::lookup(item->getInventoryType()),
- "asset_type", LLAssetType::lookup(item->getType()),
- "creation_date", (S32) item->getCreationDate(),
- "asset_id", item->getAssetUUID(),
- "is_link", item->getIsLinkType(),
- "linked_id", item->getLinkedUUID()));
+/*==========================================================================*|
+ add("getSingle",
+ "Return LLSD [\"single\"] for a single folder or item from the specified\n"
+ "[\"result\"] key at the specified 0-relative [\"index\"].",
+ &LLInventoryListener::getSingle,
+ llsd::map("result", LLSD::Integer(), "index", LLSD::Integer(),
+ "reply", LLSD::String()));
+|*==========================================================================*/
+
+ add("getSlice",
+ stringize(
+ "Return an LLSD array [\"slice\"] from the specified [\"result\"] key\n"
+ "starting at 0-relative [\"index\"] with (up to) [\"count\"] entries.\n"
+ "count is limited to ", MAX_ITEM_LIMIT, " (default and max)."),
+ &LLInventoryListener::getSlice,
+ llsd::map("result", LLSD::Integer(), "index", LLSD::Integer(),
+ "reply", LLSD::String()));
+
+ add("closeResult",
+ "Release resources associated with specified [\"result\"] key,\n"
+ "or keys if [\"result\"] is an array.",
+ &LLInventoryListener::closeResult,
+ llsd::map("result", LLSD()));
}
-void add_cat_info(LLEventAPI::Response &response, LLViewerInventoryCategory *cat)
+// This struct captures (possibly large) category results from
+// getDirectDescendants() and collectDescendantsIf().
+struct CatResultSet: public LL::ResultSet
{
- response["categories"].insert(cat->getUUID().asString(),
- llsd::map("name", cat->getName(),
- "parent_id", cat->getParentUUID(),
- "type", LLFolderType::lookup(cat->getPreferredType())));
-}
+ CatResultSet(): LL::ResultSet("categories") {}
+ LLInventoryModel::cat_array_t mCategories;
-void add_objects_info(LLEventAPI::Response& response, LLInventoryModel::cat_array_t cat_array, LLInventoryModel::item_array_t item_array)
-{
- for (auto &p : item_array)
+ int getLength() const override { return narrow(mCategories.size()); }
+ LLSD getSingle(int index) const override
{
- add_item_info(response, p);
+ auto cat = mCategories[index];
+ return llsd::map("name", cat->getName(),
+ "parent_id", cat->getParentUUID(),
+ "type", LLFolderType::lookup(cat->getPreferredType()));
}
- for (auto &p : cat_array)
+};
+
+// This struct captures (possibly large) item results from
+// getDirectDescendants() and collectDescendantsIf().
+struct ItemResultSet: public LL::ResultSet
+{
+ ItemResultSet(): LL::ResultSet("items") {}
+ LLInventoryModel::item_array_t mItems;
+
+ int getLength() const override { return narrow(mItems.size()); }
+ LLSD getSingle(int index) const override
{
- add_cat_info(response, p);
+ auto item = mItems[index];
+ return llsd::map("name", item->getName(),
+ "parent_id", item->getParentUUID(),
+ "desc", item->getDescription(),
+ "inv_type", LLInventoryType::lookup(item->getInventoryType()),
+ "asset_type", LLAssetType::lookup(item->getType()),
+ "creation_date", LLSD::Integer(item->getCreationDate()),
+ "asset_id", item->getAssetUUID(),
+ "is_link", item->getIsLinkType(),
+ "linked_id", item->getLinkedUUID());
}
-}
+};
void LLInventoryListener::getItemsInfo(LLSD const &data)
{
Response response(LLSD(), data);
+ auto catresult = new CatResultSet;
+ auto itemresult = new ItemResultSet;
+
uuid_vec_t ids = LLSDParam<uuid_vec_t>(data["item_ids"]);
for (auto &it : ids)
{
LLViewerInventoryItem* item = gInventory.getItem(it);
if (item)
{
- add_item_info(response, item);
+ itemresult->mItems.push_back(item);
}
else
{
LLViewerInventoryCategory *cat = gInventory.getCategory(it);
if (cat)
{
- add_cat_info(response, cat);
+ catresult->mCategories.push_back(cat);
}
}
}
+ // Each of categories and items is a { result set key, total length } pair.
+ response["categories"] = catresult->getKeyLength();
+ response["items"] = itemresult->getKeyLength();
}
void LLInventoryListener::getFolderTypeNames(LLSD const &data)
@@ -151,14 +190,21 @@ void LLInventoryListener::getBasicFolderID(LLSD const &data)
}
-void LLInventoryListener::getDirectDescendents(LLSD const &data)
+void LLInventoryListener::getDirectDescendants(LLSD const &data)
{
Response response(LLSD(), data);
LLInventoryModel::cat_array_t* cats;
LLInventoryModel::item_array_t* items;
gInventory.getDirectDescendentsOf(data["folder_id"], cats, items);
- add_objects_info(response, *cats, *items);
+ auto catresult = new CatResultSet;
+ auto itemresult = new ItemResultSet;
+
+ catresult->mCategories = *cats;
+ itemresult->mItems = *items;
+
+ response["categories"] = catresult->getKeyLength();
+ response["items"] = itemresult->getKeyLength();
}
struct LLFilteredCollector : public LLInventoryCollectFunctor
@@ -173,7 +219,11 @@ struct LLFilteredCollector : public LLInventoryCollectFunctor
LLFilteredCollector(LLSD const &data);
virtual ~LLFilteredCollector() {}
virtual bool operator()(LLInventoryCategory *cat, LLInventoryItem *item) override;
- virtual bool exceedsLimit() override { return (mItemLimit <= mItemCount); };
+ virtual bool exceedsLimit() override
+ {
+ // mItemLimit == 0 means unlimited
+ return (mItemLimit && mItemLimit <= mItemCount);
+ }
protected:
bool checkagainstType(LLInventoryCategory *cat, LLInventoryItem *item);
@@ -189,7 +239,7 @@ struct LLFilteredCollector : public LLInventoryCollectFunctor
S32 mItemCount;
};
-void LLInventoryListener::collectDescendentsIf(LLSD const &data)
+void LLInventoryListener::collectDescendantsIf(LLSD const &data)
{
Response response(LLSD(), data);
LLUUID folder_id(data["folder_id"].asUUID());
@@ -198,20 +248,64 @@ void LLInventoryListener::collectDescendentsIf(LLSD const &data)
{
return response.error(stringize("Folder ", std::quoted(data["folder_id"].asString()), " was not found"));
}
- LLInventoryModel::cat_array_t cat_array;
- LLInventoryModel::item_array_t item_array;
+ auto catresult = new CatResultSet;
+ auto itemresult = new ItemResultSet;
LLFilteredCollector collector = LLFilteredCollector(data);
- gInventory.collectDescendentsIf(folder_id, cat_array, item_array, LLInventoryModel::EXCLUDE_TRASH, collector);
+ // Populate results directly into the catresult and itemresult arrays.
+ // TODO: sprinkle count-based coroutine yields into the real
+ // collectDescendentsIf() method so it doesn't steal too many cycles.
+ gInventory.collectDescendentsIf(
+ folder_id,
+ catresult->mCategories,
+ itemresult->mItems,
+ LLInventoryModel::EXCLUDE_TRASH,
+ collector);
+
+ response["categories"] = catresult->getKeyLength();
+ response["items"] = itemresult->getKeyLength();
+}
+
+/*==========================================================================*|
+void LLInventoryListener::getSingle(LLSD const& data)
+{
+ auto result = LL::ResultSet::getInstance(data["result"]);
+ sendReply(llsd::map("single", result->getSingle(data["index"])), data);
+}
+|*==========================================================================*/
+
+void LLInventoryListener::getSlice(LLSD const& data)
+{
+ auto result = LL::ResultSet::getInstance(data["result"]);
+ int count = data.has("count")? data["count"].asInteger() : MAX_ITEM_LIMIT;
+ LL_DEBUGS("Lua") << *result << ".getSlice(" << data["index"].asInteger()
+ << ", " << count << ')' << LL_ENDL;
+ auto pair{ result->getSliceStart(data["index"], std::min(count, MAX_ITEM_LIMIT)) };
+ sendReply(llsd::map("slice", pair.first, "start", pair.second), data);
+}
- add_objects_info(response, cat_array, item_array);
+void LLInventoryListener::closeResult(LLSD const& data)
+{
+ LLSD results = data["result"];
+ if (results.isInteger())
+ {
+ results = llsd::array(results);
+ }
+ for (const auto& result : llsd::inArray(results))
+ {
+ auto ptr = LL::ResultSet::getInstance(result);
+ if (ptr)
+ {
+ delete ptr.get();
+ }
+ }
}
LLFilteredCollector::LLFilteredCollector(LLSD const &data) :
mType(LLAssetType::EType::AT_UNKNOWN),
mLinkFilter(INCLUDE_LINKS),
- mItemLimit(MAX_ITEM_LIMIT),
+ mItemLimit(0),
mItemCount(0)
{
@@ -235,7 +329,7 @@ LLFilteredCollector::LLFilteredCollector(LLSD const &data) :
}
if (data["limit"].isInteger())
{
- mItemLimit = llclamp(data["limit"].asInteger(), 1, MAX_ITEM_LIMIT);
+ mItemLimit = std::max(data["limit"].asInteger(), 1);
}
}
diff --git a/indra/newview/llinventorylistener.h b/indra/newview/llinventorylistener.h
index 5cbac2ca32..a05385f2c8 100644
--- a/indra/newview/llinventorylistener.h
+++ b/indra/newview/llinventorylistener.h
@@ -40,8 +40,13 @@ private:
void getFolderTypeNames(LLSD const &data);
void getAssetTypeNames(LLSD const &data);
void getBasicFolderID(LLSD const &data);
- void getDirectDescendents(LLSD const &data);
- void collectDescendentsIf(LLSD const &data);
+ void getDirectDescendants(LLSD const &data);
+ void collectDescendantsIf(LLSD const &data);
+/*==========================================================================*|
+ void getSingle(LLSD const& data);
+|*==========================================================================*/
+ void getSlice(LLSD const& data);
+ void closeResult(LLSD const& data);
};
#endif // LL_LLINVENTORYLISTENER_H
diff --git a/indra/newview/scripts/lua/require/LLInventory.lua b/indra/newview/scripts/lua/require/LLInventory.lua
index dd1b910250..2c80a8602b 100644
--- a/indra/newview/scripts/lua/require/LLInventory.lua
+++ b/indra/newview/scripts/lua/require/LLInventory.lua
@@ -1,12 +1,28 @@
local leap = require 'leap'
local mapargs = require 'mapargs'
+local result_view = require 'result_view'
+
+local function result(keys)
+ -- capture result_view() instances for both categories and items
+ local result_table = {
+ categories=result_view(keys.categories),
+ items=result_view(keys.items),
+ -- call result_table:close() to release result sets before garbage
+ -- collection or script completion
+ close = function(self)
+ result_view.close(keys.categories[1], keys.items[1])
+ end
+ }
+ -- When the result_table is destroyed, close its result_views.
+ return LL.setdtor('LLInventory result', result_table, result_table.close)
+end
local LLInventory = {}
-- Get the items/folders info by provided IDs,
-- reply will contain "items" and "categories" tables accordingly
function LLInventory.getItemsInfo(item_ids)
- return leap.request('LLInventory', {op = 'getItemsInfo', item_ids=item_ids})
+ return result(leap.request('LLInventory', {op = 'getItemsInfo', item_ids=item_ids}))
end
-- Get the table of folder type names, which can be later used to get the ID of the basic folders
@@ -19,30 +35,33 @@ function LLInventory.getBasicFolderID(ft_name)
return leap.request('LLInventory', {op = 'getBasicFolderID', ft_name=ft_name}).id
end
--- Get the table of asset type names, which can be later used to get the specific items via LLInventory.collectDescendentsIf(...)
+-- Get the table of asset type names, which can be later used to get the specific items via LLInventory.collectDescendantsIf(...)
function LLInventory.getAssetTypeNames()
return leap.request('LLInventory', {op = 'getAssetTypeNames'}).names
end
--- Get the direct descendents of the 'folder_id' provided,
+-- Get the direct descendants of the 'folder_id' provided,
-- reply will contain "items" and "categories" tables accordingly
-function LLInventory.getDirectDescendents(folder_id)
- return leap.request('LLInventory', {op = 'getDirectDescendents', folder_id=folder_id})
+function LLInventory.getDirectDescendants(folder_id)
+ return result(leap.request('LLInventory', {op = 'getDirectDescendants', folder_id=folder_id}))
end
+-- backwards compatibility
+LLInventory.getDirectDescendents = LLInventory.getDirectDescendants
--- Get the descendents of the 'folder_id' provided, which pass specified filters
+-- Get the descendants of the 'folder_id' provided, which pass specified filters
-- reply will contain "items" and "categories" tables accordingly
--- LLInventory.collectDescendentsIf{ folder_id -- parent folder ID
+-- LLInventory.collectDescendantsIf{ folder_id -- parent folder ID
-- [, name] -- name (substring)
-- [, desc] -- description (substring)
-- [, type] -- asset type
-- [, limit] -- item count limit in reply, maximum and default is 100
-- [, filter_links]} -- EXCLUDE_LINKS - don't show links, ONLY_LINKS - only show links, INCLUDE_LINKS - show links too (default)
-function LLInventory.collectDescendentsIf(...)
+function LLInventory.collectDescendantsIf(...)
local args = mapargs('folder_id,name,desc,type,filter_links,limit', ...)
- args.op = 'collectDescendentsIf'
- return leap.request('LLInventory', args)
+ args.op = 'collectDescendantsIf'
+ return result(leap.request('LLInventory', args))
end
-
+-- backwards compatibility
+LLInventory.collectDescendentsIf = LLInventory.collectDescendantsIf
return LLInventory
diff --git a/indra/newview/scripts/lua/require/UI.lua b/indra/newview/scripts/lua/require/UI.lua
index bbcae3514a..73a76fa6b8 100644
--- a/indra/newview/scripts/lua/require/UI.lua
+++ b/indra/newview/scripts/lua/require/UI.lua
@@ -2,6 +2,7 @@
local leap = require 'leap'
local mapargs = require 'mapargs'
+local result_view = require 'result_view'
local Timer = (require 'timers').Timer
local util = require 'util'
@@ -234,7 +235,9 @@ function UI.closeAllFloaters()
end
function UI.getFloaterNames()
- return leap.request("LLFloaterReg", {op = "getFloaterNames"}).floaters
+ local key_length = leap.request("LLFloaterReg", {op = "getFloaterNames"}).floaters
+ local view = result_view(key_length)
+ return LL.setdtor('registered floater names', view, view.close)
end
return UI
diff --git a/indra/newview/scripts/lua/require/result_view.lua b/indra/newview/scripts/lua/require/result_view.lua
new file mode 100644
index 0000000000..5301d7838c
--- /dev/null
+++ b/indra/newview/scripts/lua/require/result_view.lua
@@ -0,0 +1,98 @@
+local leap = require 'leap'
+
+-- metatable for every result_view() table
+local mt = {
+ __len = function(self)
+ return self.length
+ end,
+ __index = function(self, i)
+ -- right away, convert to 0-relative indexing
+ i -= 1
+ -- can we find this index within the current slice?
+ local reli = i - self.start
+ if 0 <= reli and reli < #self.slice then
+ -- Lua 1-relative indexing
+ return self.slice[reli + 1]
+ end
+ -- is this index outside the overall result set?
+ if not (0 <= i and i < self.length) then
+ return nil
+ end
+ -- fetch a new slice starting at i, using provided fetch()
+ local start
+ self.slice, start = self.fetch(self.key, i)
+ -- It's possible that caller-provided fetch() function forgot
+ -- to return the adjusted start index of the new slice. In
+ -- Lua, 0 tests as true, so if fetch() returned (slice, 0),
+ -- we'll duly reset self.start to 0. Otherwise, assume the
+ -- requested index was not adjusted: that the returned slice
+ -- really does start at i.
+ self.start = start or i
+ -- Hopefully this slice contains the desired i.
+ -- Back to 1-relative indexing.
+ return self.slice[i - self.start + 1]
+ end,
+ -- We purposely avoid putting any array entries (int keys) into
+ -- our table so that access to any int key will always call our
+ -- __index() metamethod. Moreover, we want any table iteration to
+ -- call __index(table, i) however many times; we do NOT want it to
+ -- retrieve key, length, start, slice.
+ -- So turn 'for k, v in result' into 'for k, v in ipairs(result)'.
+ __iter = ipairs,
+ -- This result set provides read-only access.
+ -- We do not support pushing updates to individual items back to
+ -- C++; for the intended use cases, that makes no sense.
+ __newindex = function(self, i, value)
+ error("result_view is a read-only data structure", 2)
+ end
+}
+
+-- result_view(key_length, fetch) returns a table which stores only a slice
+-- of a result set plus some control values, yet presents read-only virtual
+-- access to the entire result set.
+-- key_length: {result set key, total result set length}
+-- fetch: function(key, start) that returns (slice, adjusted start)
+local result_view = setmetatable(
+ {
+ -- generic fetch() function
+ fetch = function(key, start)
+ local fetched = leap.request(
+ 'LLInventory',
+ {op='getSlice', result=key, index=start})
+ return fetched.slice, fetched.start
+ end,
+ -- generic close() function accepting variadic result-set keys
+ close = function(...)
+ local keys = table.pack(...)
+ -- table.pack() produces a table with an array entry for every
+ -- parameter, PLUS an 'n' key with the count. Unfortunately that
+ -- 'n' key bollixes our conversion to LLSD, which requires either
+ -- all int keys (for an array) or all string keys (for a map).
+ keys.n = nil
+ leap.send('LLInventory', {op='closeResult', result=keys})
+ end
+ },
+ {
+ -- result_view(key_length, fetch) calls this
+ __call = function(class, key_length, fetch)
+ return setmetatable(
+ {
+ key=key_length[1],
+ length=key_length[2],
+ -- C++ result sets use 0-based indexing, so internally we do too
+ start=0,
+ -- start with a dummy array with length 0
+ slice={},
+ -- if caller didn't pass fetch() function, use generic
+ fetch=fetch or class.fetch,
+ -- returned view:close() will close result set with passed key
+ close=function(self) class.close(key_length[1]) end
+ },
+ -- use our special metatable
+ mt
+ )
+ end
+ }
+)
+
+return result_view
diff --git a/indra/newview/scripts/lua/test_LLInventory.lua b/indra/newview/scripts/lua/test_LLInventory.lua
index 107b0791d4..de57484bcd 100644
--- a/indra/newview/scripts/lua/test_LLInventory.lua
+++ b/indra/newview/scripts/lua/test_LLInventory.lua
@@ -5,7 +5,9 @@ LLInventory = require 'LLInventory'
my_landmarks_id = LLInventory.getBasicFolderID('landmark')
-- Get 3 landmarks from the 'My Landmarks' folder (you can see all folder types via LLInventory.getAssetTypeNames())
landmarks = LLInventory.collectDescendentsIf{folder_id=my_landmarks_id, type="landmark", limit=3}
-print(inspect(landmarks))
+for _, landmark in pairs(landmarks.items) do
+ print(landmark.name)
+end
-- Get 'Calling Cards' folder id
calling_cards_id = LLInventory.getBasicFolderID('callcard')
@@ -13,9 +15,10 @@ calling_cards_id = LLInventory.getBasicFolderID('callcard')
calling_cards = LLInventory.getDirectDescendents(calling_cards_id).items
-- Print a random calling card name from 'Calling Cards' folder
-local card_names = {}
-for _, value in pairs(calling_cards) do
- table.insert(card_names, value.name)
-end
+-- (because getDirectDescendents().items is a Lua result set, selecting
+-- a random entry only fetches one slice containing that entry)
math.randomseed(os.time())
-print("Random calling card: " .. inspect(card_names[math.random(#card_names)]))
+for i = 1, 5 do
+ pick = math.random(#calling_cards)
+ print(`Random calling card (#{pick} of {#calling_cards}): {calling_cards[pick].name}`)
+end
diff --git a/indra/newview/scripts/lua/test_inv_resultset.lua b/indra/newview/scripts/lua/test_inv_resultset.lua
new file mode 100644
index 0000000000..c31cfe3c67
--- /dev/null
+++ b/indra/newview/scripts/lua/test_inv_resultset.lua
@@ -0,0 +1,18 @@
+local LLInventory = require 'LLInventory'
+local inspect = require 'inspect'
+
+print('basic folders:')
+print(inspect(LLInventory.getFolderTypeNames()))
+
+local folder = LLInventory.getBasicFolderID('my_otfts')
+print(`folder = {folder}`)
+local result = LLInventory.getDirectDescendants(folder)
+print(`type(result) = {type(result)}`)
+print(#result.categories, 'categories:')
+for i, cat in pairs(result.categories) do
+ print(`{i}: {cat.name}`)
+end
+print(#result.items, 'items')
+for i, item in pairs(result.items) do
+ print(`{i}: {item.name}`)
+end
diff --git a/indra/newview/scripts/lua/test_result_view.lua b/indra/newview/scripts/lua/test_result_view.lua
new file mode 100644
index 0000000000..304633a472
--- /dev/null
+++ b/indra/newview/scripts/lua/test_result_view.lua
@@ -0,0 +1,55 @@
+-- Verify the functionality of result_view.
+result_view = require 'result_view'
+
+print('alphabet')
+alphabet = "abcdefghijklmnopqrstuvwxyz"
+assert(#alphabet == 26)
+alphabits = string.split(alphabet, '')
+
+print('function slice()')
+function slice(t, index, count)
+ return table.move(t, index, index + count - 1, 1, {})
+end
+
+print('verify slice()')
+-- verify that slice() does what we expect
+assert(table.concat(slice(alphabits, 4, 3)) == "def")
+assert(table.concat(slice(alphabits, 14, 3)) == "nop")
+assert(table.concat(slice(alphabits, 25, 3)) == "yz")
+
+print('function fetch()')
+function fetch(key, index)
+ -- fetch function is defined to be 0-relative: fix for Lua data
+ -- constrain view of alphabits to slices of at most 3 elements
+ return slice(alphabits, index+1, 3), index
+end
+
+print('result_view()')
+-- for test purposes, key is irrelevant, so just 'key'
+view = result_view({'key', #alphabits}, fetch)
+
+print('function check_iter()')
+function check_iter(...)
+ result = {}
+ for k, v in ... do
+ table.insert(result, v)
+ end
+ assert(table.concat(result) == alphabet)
+end
+
+print('check_iter(pairs(view))')
+check_iter(pairs(view))
+print('check_iter(ipairs(view))')
+check_iter(ipairs(view))
+print('check_iter(view)')
+check_iter(view)
+
+print('raw index access')
+assert(view[5] == 'e')
+assert(view[10] == 'j')
+assert(view[15] == 'o')
+assert(view[20] == 't')
+assert(view[25] == 'y')
+
+print('Success!')
+
diff --git a/indra/newview/scripts/lua/test_setdtor.lua b/indra/newview/scripts/lua/test_setdtor.lua
new file mode 100644
index 0000000000..ec5cd47e93
--- /dev/null
+++ b/indra/newview/scripts/lua/test_setdtor.lua
@@ -0,0 +1,91 @@
+inspect = require 'inspect'
+
+print('initial setdtor')
+bye = LL.setdtor('initial setdtor', 'Goodbye world!', print)
+
+print('arithmetic')
+n = LL.setdtor('arithmetic', 11, print)
+print("n =", n)
+print("n._target =", n._target)
+print(pcall(function() n._target = 12 end))
+print("getmetatable(n) =", inspect(getmetatable(n)))
+print("-n =", -n)
+for i = 10, 12 do
+ -- Comparison metamethods are only called if both operands have the same
+ -- metamethod.
+ tempi = LL.setdtor('tempi', i, function(n) print('temp', i) end)
+ print(`n < {i}`, n < tempi)
+ print(`n <= {i}`, n <= tempi)
+ print(`n == {i}`, n == tempi)
+ print(`n ~= {i}`, n ~= tempi)
+ print(`n >= {i}`, n >= tempi)
+ print(`n > {i}`, n > tempi)
+end
+i = 2
+print(`n + {i} =`, n + i)
+print(`{i} + n =`, i + n)
+print(`n - {i} =`, n - i)
+print(`{i} - n =`, i - n)
+print(`n * {i} =`, n * i)
+print(`{i} * n =`, i * n)
+print(`n / {i} =`, n / i)
+print(`{i} / n =`, i / n)
+print(`n // {i} =`, n // i)
+print(`{i} // n =`, i // n)
+print(`n % {i} =`, n % i)
+print(`{i} % n =`, i % n)
+print(`n ^ {i} =`, n ^ i)
+print(`{i} ^ n =`, i ^ n)
+
+print('string')
+s = LL.setdtor('string', 'hello', print)
+print('s =', s)
+print('#s =', #s)
+print('s .. " world" =', s .. " world")
+print('"world " .. s =', "world " .. s)
+
+print('table')
+t = LL.setdtor('table', {'[1]', '[2]', abc='.abc', def='.def'},
+ function(t) print(inspect(t)) end)
+print('t =', inspect(t))
+print('t._target =', inspect(t._target))
+print('#t =', #t)
+print('next(t) =', next(t))
+print('next(t, 1) =', next(t, 1))
+print('t[2] =', t[2])
+print('t.def =', t.def)
+t[1] = 'new [1]'
+print('t[1] =', t[1])
+print('for k, v in pairs(t) do')
+for k, v in pairs(t) do
+ print(`{k}: {v}`)
+end
+print('for k, v in ipairs(t) do')
+for k, v in ipairs(t) do
+ print(`{k}: {v}`)
+end
+print('for k, v in t do')
+for k, v in t do
+ print(`{k}: {v}`)
+end
+-- and now for something completely different
+setmetatable(
+ t._target,
+ {
+ __iter = function(arg)
+ return next, {'alternate', '__iter'}
+ end
+ }
+)
+print('for k, v in t with __iter() metamethod do')
+for k, v in t do
+ print(`{k}: {v}`)
+end
+
+print('function')
+f = LL.setdtor('function', function(a, b) return (a .. b) end, print)
+print('f =', f)
+print('f._target =', f._target)
+print('f("Hello", " world") =', f("Hello", " world"))
+
+print('cleanup')
diff --git a/indra/newview/tests/llluamanager_test.cpp b/indra/newview/tests/llluamanager_test.cpp
index 8ce5c357e0..8d1333815b 100644
--- a/indra/newview/tests/llluamanager_test.cpp
+++ b/indra/newview/tests/llluamanager_test.cpp
@@ -463,8 +463,8 @@ namespace tut
// but now we have to give the startScriptLine() coroutine a chance to run
auto [count, result] = future.get();
ensure_equals("killed Lua script terminated normally", count, -1);
- ensure_equals("unexpected killed Lua script error",
- result.asString(), "viewer is stopping");
+ ensure_contains("unexpected killed Lua script error",
+ result.asString(), "viewer is stopping");
}
template<> template<>