// This file is part of the lluz programming language and is licensed under MIT License; see LICENSE.txt for details #include "lua.h" #include "lualib.h" #include "Repl.h" #include "doctest.h" #include #include #include #include #include struct Completion { std::string completion; std::string display; bool operator<(Completion const& other) const { return std::tie(completion, display) < std::tie(other.completion, other.display); } }; using CompletionSet = std::set; class ReplFixture { public: ReplFixture() : luaState(luaL_newstate(), lua_close) { L = luaState.get(); setupState(L); luaL_sandboxthread(L); std::string result = runCode(L, prettyPrintSource); } // Returns all of the output captured from the pretty printer std::string getCapturedOutput() { lua_getglobal(L, XorStr("capturedoutput")); const char* str = lua_tolstring(L, -1, nullptr); std::string result(str); lua_pop(L, 1); return result; } CompletionSet getCompletionSet(const char* inputPrefix) { CompletionSet result; int top = lua_gettop(L); getCompletions(L, inputPrefix, [&result](const std::string& completion, const std::string& display) { result.insert(Completion{completion, display}); }); // Ensure that generating completions doesn't change the position of lluz's stack top. CHECK(top == lua_gettop(L)); return result; } bool checkCompletion(const CompletionSet& completions, const std::string& prefix, const std::string& expected) { std::string expectedDisplay(expected.substr(0, expected.find_first_of('('))); Completion expectedCompletion{prefix + expected, expectedDisplay}; return completions.count(expectedCompletion) == 1; } lua_State* L; private: std::unique_ptr luaState; // This is a simplistic and incomplete pretty printer. // It is included here to test that the pretty printer hook is being called. // More elaborate tests to ensure correct output can be added if we introduce // a more feature rich pretty printer. std::string prettyPrintSource = R"( -- Accumulate pretty printer output in `capturedoutput` capturedoutput = "" function arraytostring(arr) local strings = {} table.foreachi(arr, function(k,v) table.insert(strings, pptostring(v)) end ) return XorStr("{") .. table.concat(strings, ", ") .. "}" end function pptostring(x) if type(x) == XorStr("table") then -- Just assume array-like tables for now. return arraytostring(x) elseif type(x) == XorStr("string") then return '"' .. x .. '"' else return tostring(x) end end -- Note: Instead of calling print, the pretty printer just stores the output -- in `capturedoutput` so we can check for the correct results. function _PRETTYPRINT(...) local args = table.pack(...) local strings = {} for i=1, args.n do local item = args[i] local str = pptostring(item, customoptions) if i == 1 then capturedoutput = capturedoutput .. str else capturedoutput = capturedoutput .. "\t" .. str end end end )"; }; TEST_SUITE_BEGIN(XorStr("ReplPrettyPrint")); TEST_CASE_FIXTURE(ReplFixture, "AdditionStatement") { runCode(L, XorStr("return 30 + 12")); CHECK(getCapturedOutput() == XorStr("42")); } TEST_CASE_FIXTURE(ReplFixture, "TableLiteral") { runCode(L, XorStr("return {1, 2, 3, 4}")); CHECK(getCapturedOutput() == XorStr("{1, 2, 3, 4}")); } TEST_CASE_FIXTURE(ReplFixture, "StringLiteral") { runCode(L, XorStr("return 'str'")); CHECK(getCapturedOutput() == XorStr("\"str\"")); } TEST_CASE_FIXTURE(ReplFixture, "TableWithStringLiterals") { runCode(L, XorStr("return {1, 'two', 3, 'four'}")); CHECK(getCapturedOutput() == XorStr("{1, \"two\", 3, \"four\"}")); } TEST_CASE_FIXTURE(ReplFixture, "MultipleArguments") { runCode(L, XorStr("return 3, 'three'")); CHECK(getCapturedOutput() == XorStr("3\t\"three\"")); } TEST_SUITE_END(); TEST_SUITE_BEGIN(XorStr("ReplCodeCompletion")); TEST_CASE_FIXTURE(ReplFixture, "CompleteGlobalVariables") { runCode(L, R"( myvariable1 = 5 myvariable2 = 5 )"); { // Try to complete globals that are added by the user's script CompletionSet completions = getCompletionSet(XorStr("myvar")); std::string prefix = ""; CHECK(completions.size() == 2); CHECK(checkCompletion(completions, prefix, "myvariable1")); CHECK(checkCompletion(completions, prefix, "myvariable2")); } { // Try completing some builtin functions CompletionSet completions = getCompletionSet(XorStr("math.m")); std::string prefix = "math."; CHECK(completions.size() == 3); CHECK(checkCompletion(completions, prefix, "max(")); CHECK(checkCompletion(completions, prefix, "min(")); CHECK(checkCompletion(completions, prefix, "modf(")); } } TEST_CASE_FIXTURE(ReplFixture, "CompleteTableKeys") { runCode(L, R"( t = { color = "red", size = 1, shape = "circle" } )"); { CompletionSet completions = getCompletionSet(XorStr("t.")); std::string prefix = "t."; CHECK(completions.size() == 3); CHECK(checkCompletion(completions, prefix, "color")); CHECK(checkCompletion(completions, prefix, "size")); CHECK(checkCompletion(completions, prefix, "shape")); } { CompletionSet completions = getCompletionSet(XorStr("t.s")); std::string prefix = "t."; CHECK(completions.size() == 2); CHECK(checkCompletion(completions, prefix, "size")); CHECK(checkCompletion(completions, prefix, "shape")); } } TEST_CASE_FIXTURE(ReplFixture, "StringMethods") { runCode(L, R"( s = "" )"); { CompletionSet completions = getCompletionSet(XorStr("s:l")); std::string prefix = "s:"; CHECK(completions.size() == 2); CHECK(checkCompletion(completions, prefix, "len(")); CHECK(checkCompletion(completions, prefix, "lower(")); } } TEST_CASE_FIXTURE(ReplFixture, "TableWithMetatableIndexTable") { runCode(L, R"( -- Create 't' which is a table with a metatable with an __index table mt = {} mt.__index = mt t = {} setmetatable(t, mt) mt.mtkey1 = {x="x value", y="y value", 1, 2} mt.mtkey2 = 2 t.tkey1 = {data1 = 2, data2 = "str", 3, 4} t.tkey2 = 4 )"); { CompletionSet completions = getCompletionSet(XorStr("t.t")); std::string prefix = "t."; CHECK(completions.size() == 2); CHECK(checkCompletion(completions, prefix, "tkey1")); CHECK(checkCompletion(completions, prefix, "tkey2")); } { CompletionSet completions = getCompletionSet(XorStr("t.tkey1.data2:re")); std::string prefix = "t.tkey1.data2:"; CHECK(completions.size() == 2); CHECK(checkCompletion(completions, prefix, "rep(")); CHECK(checkCompletion(completions, prefix, "reverse(")); } { CompletionSet completions = getCompletionSet(XorStr("t.mtk")); std::string prefix = "t."; CHECK(completions.size() == 2); CHECK(checkCompletion(completions, prefix, "mtkey1")); CHECK(checkCompletion(completions, prefix, "mtkey2")); } { CompletionSet completions = getCompletionSet(XorStr("t.mtkey1.")); std::string prefix = "t.mtkey1."; CHECK(completions.size() == 2); CHECK(checkCompletion(completions, prefix, "x")); CHECK(checkCompletion(completions, prefix, "y")); } } TEST_CASE_FIXTURE(ReplFixture, "TableWithMetatableIndexFunction") { runCode(L, R"( -- Create 't' which is a table with a metatable with an __index function mt = {} mt.__index = function(table, key) print("mt.__index called") if key == XorStr("foo") then return XorStr("FOO") elseif key == XorStr("bar") then return XorStr("BAR") else return nil end end t = {} setmetatable(t, mt) t.tkey = 0 )"); { CompletionSet completions = getCompletionSet(XorStr("t.t")); std::string prefix = "t."; CHECK(completions.size() == 1); CHECK(checkCompletion(completions, prefix, "tkey")); } { // t.foo is a valid key, but should not be completed because it requires calling an __index function CompletionSet completions = getCompletionSet(XorStr("t.foo")); CHECK(completions.size() == 0); } { // t.foo is a valid key, but should not be found because it requires calling an __index function CompletionSet completions = getCompletionSet(XorStr("t.foo:")); CHECK(completions.size() == 0); } } TEST_CASE_FIXTURE(ReplFixture, "TableWithMultipleMetatableIndexTables") { runCode(L, R"( -- Create a table with a chain of metatables mt2 = {} mt2.__index = mt2 mt = {} mt.__index = mt setmetatable(mt, mt2) t = {} setmetatable(t, mt) mt2.mt2key = {x=1, y=2} mt.mtkey = 2 t.tkey = 3 )"); { CompletionSet completions = getCompletionSet(XorStr("t.")); std::string prefix = "t."; CHECK(completions.size() == 4); CHECK(checkCompletion(completions, prefix, "__index")); CHECK(checkCompletion(completions, prefix, "tkey")); CHECK(checkCompletion(completions, prefix, "mtkey")); CHECK(checkCompletion(completions, prefix, "mt2key")); } { CompletionSet completions = getCompletionSet(XorStr("t.__index.")); std::string prefix = "t.__index."; CHECK(completions.size() == 3); CHECK(checkCompletion(completions, prefix, "__index")); CHECK(checkCompletion(completions, prefix, "mtkey")); CHECK(checkCompletion(completions, prefix, "mt2key")); } { CompletionSet completions = getCompletionSet(XorStr("t.mt2key.")); std::string prefix = "t.mt2key."; CHECK(completions.size() == 2); CHECK(checkCompletion(completions, prefix, "x")); CHECK(checkCompletion(completions, prefix, "y")); } } TEST_CASE_FIXTURE(ReplFixture, "TableWithDeepMetatableIndexTables") { runCode(L, R"( -- Creates a table with a chain of metatables of length `count` function makeChainedTable(count) local result = {} result.__index = result result[string.format(XorStr("entry%d"), count)] = { count = count } if count == 0 then return result else return setmetatable(result, makeChainedTable(count - 1)) end end t30 = makeChainedTable(30) t60 = makeChainedTable(60) )"); { // Check if entry0 exists CompletionSet completions = getCompletionSet(XorStr("t30.entry0")); std::string prefix = "t30."; CHECK(checkCompletion(completions, prefix, "entry0")); } { // Check if entry0.count exists CompletionSet completions = getCompletionSet(XorStr("t30.entry0.co")); std::string prefix = "t30.entry0."; CHECK(checkCompletion(completions, prefix, "count")); } { // Check if entry0 exists. With the max traversal limit of 50 in the repl, this should fail. CompletionSet completions = getCompletionSet(XorStr("t60.entry0")); CHECK(completions.size() == 0); } { // Check if entry0.count exists. With the max traversal limit of 50 in the repl, this should fail. CompletionSet completions = getCompletionSet(XorStr("t60.entry0.co")); CHECK(completions.size() == 0); } } TEST_SUITE_END();