luau/tests/Repl.test.cpp
aaron 8f94786ceb
Some checks failed
benchmark / callgrind (map[branch:main name:luau-lang/benchmark-data], ubuntu-22.04) (push) Has been cancelled
build / macos (push) Has been cancelled
build / macos-arm (push) Has been cancelled
build / ubuntu (push) Has been cancelled
build / windows (Win32) (push) Has been cancelled
build / windows (x64) (push) Has been cancelled
build / coverage (push) Has been cancelled
build / web (push) Has been cancelled
release / macos (push) Has been cancelled
release / ubuntu (push) Has been cancelled
release / windows (push) Has been cancelled
release / web (push) Has been cancelled
Refactor CLI structure to match the include/src split that our other projects have. (#1573)
This PR refactors the CLI folder to use the same project split between
include and src directories that we have for all the other artifacts in
luau. It also includes the require-by-string implementation we already
have as a feature of `Luau.CLI.lib`. Both of these changes are targeted
at making it easier for embedding projects to setup an effective
equivalent to the standalone `luau` executable with whatever runtime
libraries they need attached and without having to unnecessarily
duplicate code from luau itself.
2024-12-17 13:50:27 -08:00

451 lines
12 KiB
C++

// 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 "Luau/Repl.h"
#include "ScopedFlags.h"
#include "doctest.h"
#include <iostream>
#include <memory>
#include <set>
#include <string>
#include <vector>
LUAU_FASTFLAG(LuauMathMap)
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;
}
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 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;
// 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 "{" .. 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();
TEST_SUITE_BEGIN("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("myvar");
std::string prefix = "";
CHECK(completions.size() == 2);
CHECK(checkCompletion(completions, prefix, "myvariable1"));
CHECK(checkCompletion(completions, prefix, "myvariable2"));
}
if (FFlag::LuauMathMap)
{
// Try completing some builtin functions
CompletionSet completions = getCompletionSet("math.m");
std::string prefix = "math.";
CHECK(completions.size() == 4);
CHECK(checkCompletion(completions, prefix, "max("));
CHECK(checkCompletion(completions, prefix, "min("));
CHECK(checkCompletion(completions, prefix, "modf("));
CHECK(checkCompletion(completions, prefix, "map("));
}
}
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("));
}
}
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);
}
}
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)
)");
}
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();