luau/tests/Repl.test.cpp

452 lines
12 KiB
C++
Raw Permalink Normal View History

// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "lua.h"
#include "lualib.h"
#include "Repl.h"
#include "ScopedFlags.h"
#include "doctest.h"
#include <iostream>
#include <memory>
2022-02-12 03:02:09 +08:00
#include <set>
#include <string>
#include <vector>
LUAU_FASTFLAG(LuauMathMap)
2022-02-12 03:02:09 +08:00
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<Completion>;
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, "capturedoutput");
const char* str = lua_tolstring(L, -1, nullptr);
std::string result(str);
lua_pop(L, 1);
return result;
}
2022-02-12 03:02:09 +08:00
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});
}
);
2022-02-12 03:02:09 +08:00
// Ensure that generating completions doesn't change the position of luau'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<lua_State, void (*)(lua_State*)> luaState;
2022-02-18 09:18:01 +08:00
// 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)
2022-08-05 06:35:33 +08:00
local strings = {}
table.foreachi(arr, function(k,v) table.insert(strings, pptostring(v)) end )
return "{" .. table.concat(strings, ", ") .. "}"
end
function pptostring(x)
if type(x) == "table" then
-- Just assume array-like tables for now.
return arraytostring(x)
elseif type(x) == "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("ReplPrettyPrint");
TEST_CASE_FIXTURE(ReplFixture, "AdditionStatement")
{
runCode(L, "return 30 + 12");
CHECK(getCapturedOutput() == "42");
}
TEST_CASE_FIXTURE(ReplFixture, "TableLiteral")
{
runCode(L, "return {1, 2, 3, 4}");
CHECK(getCapturedOutput() == "{1, 2, 3, 4}");
}
TEST_CASE_FIXTURE(ReplFixture, "StringLiteral")
{
runCode(L, "return 'str'");
CHECK(getCapturedOutput() == "\"str\"");
}
TEST_CASE_FIXTURE(ReplFixture, "TableWithStringLiterals")
{
runCode(L, "return {1, 'two', 3, 'four'}");
CHECK(getCapturedOutput() == "{1, \"two\", 3, \"four\"}");
}
TEST_CASE_FIXTURE(ReplFixture, "MultipleArguments")
{
runCode(L, "return 3, 'three'");
CHECK(getCapturedOutput() == "3\t\"three\"");
}
TEST_SUITE_END();
2022-02-12 03:02:09 +08:00
TEST_SUITE_BEGIN("ReplCodeCompletion");
TEST_CASE_FIXTURE(ReplFixture, "CompleteGlobalVariables")
{
runCode(L, R"(
myvariable1 = 5
myvariable2 = 5
)");
2022-02-18 09:18:01 +08:00
{
// Try to complete globals that are added by the user's script
CompletionSet completions = getCompletionSet("myvar");
std::string prefix = "";
CHECK(completions.size() == 2);
CHECK(checkCompletion(completions, prefix, "myvariable1"));
CHECK(checkCompletion(completions, prefix, "myvariable2"));
}
if (FFlag::LuauMathMap)
2022-02-18 09:18:01 +08:00
{
// Try completing some builtin functions
CompletionSet completions = getCompletionSet("math.m");
2022-02-12 03:02:09 +08:00
2022-02-18 09:18:01 +08:00
std::string prefix = "math.";
CHECK(completions.size() == 4);
2022-02-18 09:18:01 +08:00
CHECK(checkCompletion(completions, prefix, "max("));
CHECK(checkCompletion(completions, prefix, "min("));
CHECK(checkCompletion(completions, prefix, "modf("));
CHECK(checkCompletion(completions, prefix, "map("));
2022-02-18 09:18:01 +08:00
}
2022-02-12 03:02:09 +08:00
}
TEST_CASE_FIXTURE(ReplFixture, "CompleteTableKeys")
{
runCode(L, R"(
t = { color = "red", size = 1, shape = "circle" }
)");
{
CompletionSet completions = getCompletionSet("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("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("s:l");
std::string prefix = "s:";
CHECK(completions.size() == 2);
CHECK(checkCompletion(completions, prefix, "len("));
CHECK(checkCompletion(completions, prefix, "lower("));
}
}
2022-02-18 09:18:01 +08:00
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("t.t");
std::string prefix = "t.";
CHECK(completions.size() == 2);
CHECK(checkCompletion(completions, prefix, "tkey1"));
CHECK(checkCompletion(completions, prefix, "tkey2"));
}
{
CompletionSet completions = getCompletionSet("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("t.mtk");
std::string prefix = "t.";
CHECK(completions.size() == 2);
CHECK(checkCompletion(completions, prefix, "mtkey1"));
CHECK(checkCompletion(completions, prefix, "mtkey2"));
}
{
CompletionSet completions = getCompletionSet("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 == "foo" then
return "FOO"
elseif key == "bar" then
return "BAR"
else
return nil
end
end
t = {}
setmetatable(t, mt)
t.tkey = 0
)");
{
CompletionSet completions = getCompletionSet("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("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("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("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("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("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("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("t30.entry0");
std::string prefix = "t30.";
CHECK(checkCompletion(completions, prefix, "entry0"));
}
{
// Check if entry0.count exists
CompletionSet completions = getCompletionSet("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("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("t60.entry0.co");
CHECK(completions.size() == 0);
}
}
2022-02-12 03:02:09 +08:00
TEST_SUITE_END();
TEST_SUITE_BEGIN("RegressionTests");
TEST_CASE_FIXTURE(ReplFixture, "InfiniteRecursion")
{
// If the infinite recrusion is not caught, test will fail
runCode(L, R"(
local NewProxyOne = newproxy(true)
local MetaTableOne = getmetatable(NewProxyOne)
MetaTableOne.__index = function()
return NewProxyOne.Game
end
print(NewProxyOne.HelloICauseACrash)
)");
}
Sync to upstream/release/628 (#1278) ### What's new? * Remove a case of unsound `table.move` optimization * Add Luau stack slot reservations that were missing in REPL (fixes #1273) ### New Type Solver * Assignments have been completely reworked to fix a case of cyclic constraint dependency * When indexing, if the fresh type's upper bound already contains a compatible indexer, do not add another upper bound * Distribute type arguments over all type families sans `eq`, `keyof`, `rawkeyof`, and other internal type families * Fix a case where `buffers` component weren't read in two places (fixes #1267) * Fix a case where things that constitutes a strong ref were slightly incorrect * Fix a case where constraint dependencies weren't setup wrt `for ... in` statement ### Native Codegen * Fix an optimization that splits TValue store only when its value and its tag are compatible * Implement a system to plug additional type information for custom host userdata types --- ### Internal Contributors Co-authored-by: Aaron Weiss <aaronweiss@roblox.com> Co-authored-by: Alexander McCord <amccord@roblox.com> Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Vighnesh Vijay <vvijay@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com> --------- Co-authored-by: Aaron Weiss <aaronweiss@roblox.com> Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Vighnesh <vvijay@roblox.com> Co-authored-by: Aviral Goel <agoel@roblox.com> Co-authored-by: David Cope <dcope@roblox.com> Co-authored-by: Lily Brown <lbrown@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
2024-06-01 03:18:18 +08:00
TEST_CASE_FIXTURE(ReplFixture, "InteractiveStackReserve1")
{
// Reset stack reservation
lua_resume(L, nullptr, 0);
runCode(L, R"(
local t = {}
)");
}
TEST_CASE_FIXTURE(ReplFixture, "InteractiveStackReserve2")
{
// Reset stack reservation
lua_resume(L, nullptr, 0);
getCompletionSet("a");
}
TEST_SUITE_END();