feat: test setup using frktest & more

* Sets up a test runner system and includes unit tests for existing
  generators using Rojo's test files.
* Configure Luau analysis and disables unnecessary lints.
* Make sync config generator accept a third options argument with a
  `force` option to generate configs even when there is a
  `default.project.json` present in the `projectDir`.
This commit is contained in:
Erica Marigold 2024-12-03 14:58:15 +00:00
parent 5f068ba1b8
commit 83ab6bf97f
Signed by: DevComp
GPG key ID: 429EF1C337871656
13 changed files with 277 additions and 30 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*_packages/
pesde.lock

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "test-files/rojo"]
path = test-files/rojo
url = https://github.com/rojo-rbx/rojo.git

7
.luaurc Normal file
View file

@ -0,0 +1,7 @@
{
"languageMode": "nonstrict",
"lint": {
"*": true,
"TableOperations": false
}
}

View file

@ -5,15 +5,13 @@ local function enter(fn: (args: { string }) -> number?): never
local startTime = os.clock() local startTime = os.clock()
local exitCode = fn(table.clone(process.args)) local exitCode = fn(table.clone(process.args))
stdio.write( stdio.write(`done in {stdio.style("dim")}{string.format("%.2fs", os.clock() - startTime)}{stdio.style("reset")}!\n`)
`done in {stdio.style("dim")}{string.format("%.2fs", os.clock() - startTime)}{stdio.style("reset")}!\n`
)
return process.exit(exitCode) return process.exit(exitCode)
end end
return enter(function(args: { string }): number? return enter(function(args: { string }): number?
local ok, _ = require("../src").generators.rojo.syncConfig(table.remove(args, 1), args, true) local ok, _ = require("../src").generators.rojo.syncConfig(table.remove(args, 1), args, { writeToFile = true })
return tonumber(ok) return tonumber(ok)
end) end)

74
.lune/tests/init.luau Normal file
View file

@ -0,0 +1,74 @@
--> Run tests using frktest runner
local fs = require("@lune/fs")
local process = require("@lune/process")
local frktest = require("../../lune_packages/frktest")
local reporter = require("./reporter")
-- HACK: Cast require to allow for dynamic paths in strict mode
-- A more proper solution would be to use luau.load instead, but
-- frktest requires its global state to be modified by test suites
local require = require :: (
path: string
) -> (
test: typeof(setmetatable(
{} :: {
case: (name: string, fn: () -> nil) -> (),
suite: (name: string, fn: () -> ()) -> (),
},
{ __index = frktest.test }
))
) -> ()
local function discoverTests(dir: string)
local tests = {}
for _, file in fs.readDir(dir) do
local fullPath = `{dir}/{file}`
-- Look for files ending in `.spec.luau` as tests
if fs.isFile(fullPath) and string.sub(file, -10) == ".spec.luau" then
table.insert(tests, fullPath)
end
-- Recurse for directories
if fs.isDir(fullPath) then
local moreTests = discoverTests(fullPath)
-- Why are the indices starting at 0???? What????
table.move(moreTests, 0, #moreTests, #tests, tests)
end
end
return tests
end
local allowedTests = process.args
for _, test in discoverTests("src") do
-- If we are given any arguments, we only run those tests, otherwise,
-- we run all the tests
-- So, to include only a certain set of test files, you can provide either
-- the full path to the test file (with or without the extension) or the test
-- file name
local withoutExt = string.sub(test, 1, -6)
local isAllowed = #process.args == 0
or table.find(allowedTests, `tests/{test}`)
or table.find(allowedTests, withoutExt)
or table.find(allowedTests, `tests/{withoutExt}`)
local constructors = {
case = frktest.test.case,
suite = frktest.test.suite,
}
if not isAllowed then
constructors.case = frktest.test.skip.case
constructors.suite = frktest.test.skip.suite
end
require(`../../{test}`)(setmetatable(constructors, { __index = frktest.test }))
end
reporter.init()
process.exit(tonumber(frktest.run()))

45
.lune/tests/reporter.luau Normal file
View file

@ -0,0 +1,45 @@
local stdio = require("@lune/stdio")
local frktest = require("../../lune_packages/frktest")
local Reporter = frktest._reporters.lune_console_reporter
local STYLE = table.freeze({
suite = function(name: string)
return `{stdio.style("bold")}{stdio.color("purple")}SUITE{stdio.style("reset")} {name}`
end,
report = function(name: string, state: "run" | "success" | "error" | "skip")
local state_color: stdio.Color = if state == "run"
then "white"
elseif state == "success" then "green"
elseif state == "error" then "red"
elseif state == "skip" then "yellow"
else error("Invalid test state")
return ` {stdio.style("bold")}{stdio.color(state_color)}{if state == "skip" then "SKIP" else "TEST"}{stdio.style(
"reset"
)} {name}`
end,
})
local ReporterExt = {}
function ReporterExt.init()
frktest.test.on_suite_enter(function(suite)
print(STYLE.suite(suite.name))
end)
frktest.test.on_suite_leave(function()
stdio.write("\n")
end)
frktest.test.on_test_leave(function(test)
print(STYLE.report(test.name, if test.failed then "error" else "success"))
end)
frktest.test.on_test_skipped(function(test)
print(STYLE.report(test.name, "skip"))
end)
Reporter.init()
end
return setmetatable(ReporterExt, { __index = Reporter })

View file

@ -5,7 +5,7 @@ description = "Scripts and other utilities for use with pesde"
authors = ["dai <contact@daimond113.com> (https://www.daimond113.com/)", "Erica Marigold <hi@devcomp.xyz>"] authors = ["dai <contact@daimond113.com> (https://www.daimond113.com/)", "Erica Marigold <hi@devcomp.xyz>"]
repository = "https://github.com/pesde-pkg/scripts" repository = "https://github.com/pesde-pkg/scripts"
license = "MIT" license = "MIT"
includes = ["src/**/*.luau", "README.md", "LICENSE.md"] includes = ["src/**/*.luau", "pesde.toml", "README.md", "LICENSE.md"]
[target] [target]
environment = "lune" environment = "lune"
@ -13,3 +13,6 @@ lib = "src/init.luau"
[indices] [indices]
default = "https://github.com/daimond113/pesde-index" default = "https://github.com/daimond113/pesde-index"
[dev_dependencies]
frktest = { name = "itsfrank/frktest", version = "^0.0.2" }

View file

@ -16,14 +16,10 @@ local PATH_ACTION_MAP: { [string]: (dir: string) -> number? } = {
end, end,
["init.lua"] = function() ["init.lua"] = function()
return stdio.write( return stdio.write(serde.encode("json", { filePaths = { "init.lua" } }, false))
serde.encode("json", { filePaths = { "init.lua" } }, false)
)
end, end,
["init.luau"] = function() ["init.luau"] = function()
return stdio.write( return stdio.write(serde.encode("json", { filePaths = { "init.luau" } }, false))
serde.encode("json", { filePaths = { "init.luau" } }, false)
)
end, end,
} }
@ -32,6 +28,7 @@ local PATH_ACTION_MAP: { [string]: (dir: string) -> number? } = {
--- ## Errors --- ## Errors
--- * The current process lacks permissions to a file --- * The current process lacks permissions to a file
--- * Failure to spawn `rojo` command
--- * Any I/O error occurs --- * Any I/O error occurs
return function(packageDirectory: string?): boolean return function(packageDirectory: string?): boolean
packageDirectory = packageDirectory or process.cwd packageDirectory = packageDirectory or process.cwd

View file

@ -0,0 +1,87 @@
local fs = require("@lune/fs")
local luau = require("@lune/luau")
local process = require("@lune/process")
local regex = require("@lune/regex")
local serde = require("@lune/serde")
local frktest = require("../../../lune_packages/frktest")
local check = frktest.assert.check
local TEST_PROJECTS_DIR = "./test-files/rojo/test-projects"
local TEST_PROJECT_EXCLUDES = {
"json_model",
"bad_json_model",
"plugins",
"legacy-0.5.x-unknown-names",
}
local BUILTIN_PATCHES: {
[string]: { [string]: (...any) -> ...any? },
} = {
["@lune/process"] = {
spawn = function(program: string, params: { string }, options: process.SpawnOptions): process.SpawnResult
local patchedOptions: process.SpawnOptions = options or {}
patchedOptions.stdio = "default"
local result = process.spawn(program, params, patchedOptions)
-- First we make sure the command exited properly
assert(result.ok, `Expected \`rojo\` command to not error, got:\n{string.rep(" ", 6)}{result.stderr}`)
-- We also make sure that the output JSON was valid
serde.decode("json", result.stdout)
return result
end,
},
["@lune/stdio"] = {
write = function(msg: string)
-- Only make sure output JSON is valid
serde.decode("json", msg)
end,
},
}
local function requireWithPatches(path: string)
for builtin, patch in BUILTIN_PATCHES do
if path == builtin then
return setmetatable(patch, { __index = require(builtin) })
end
end
return require(path)
end
return function(test: typeof(frktest.test))
test.suite("Generates Rojo sourcemaps for test projects successfully", function()
for _, file in fs.readDir(TEST_PROJECTS_DIR) do
if table.find(TEST_PROJECT_EXCLUDES, file) then
-- It does not make sense to test sourcemap generation for some of the test projects
continue
end
local testProject = `{TEST_PROJECTS_DIR}/{file}`
test.case(file, function()
-- If a file starts with `bad_` but not `bad_meta_`, we should expect a failure
-- Also, sorry about this shitty regex, regex-rs does not support look-around :(
local isBadMeta = regex.new("^bad_[^m]|^bad_m[^e]|^bad_me[^t]|^bad_met[^a]")
local expect = if isBadMeta:isMatch(file) then check.should_error else check.should_not_error
expect(function()
-- We override the require which applies some patches to some builtins, mainly preventing
-- command stdout forwarding and `stdio.write` outputs to stdout in order to not fill
-- test ouput with large amounts of JSON data
local sourcemap: (string) -> boolean =
luau.load(fs.readFile("./src/generators/rojo/sourcemap.luau"), {
environment = {
require = requireWithPatches,
},
})()
return check.is_true(sourcemap(testProject))
end)
end)
end
end)
end

View file

@ -56,15 +56,13 @@ local PLATFORM_SEP = if process.os == "windows" then "\\" else "/"
--- ## Errors --- ## Errors
--- * The current process lacks permissions to a file --- * The current process lacks permissions to a file
--- * Any I/O error occurs --- * Any I/O error occurs
return function( return function(packageDirectory: string?, files: { string }, options: {
packageDirectory: string?, writeToFile: boolean?,
files: { string }, force: boolean?,
writeToFile: boolean? }): (boolean, string?)
): (boolean, string?)
packageDirectory = packageDirectory or process.cwd packageDirectory = packageDirectory or process.cwd
local syncConfigPath = local syncConfigPath = `{packageDirectory}{PLATFORM_SEP}default.project.json`
`{packageDirectory}{PLATFORM_SEP}default.project.json` if fs.isFile(syncConfigPath) and not options.force then
if fs.isFile(syncConfigPath) then
return true, nil return true, nil
end end
@ -102,9 +100,8 @@ return function(
-- Finally, we serialize the config to a JSON string and optionally write it -- Finally, we serialize the config to a JSON string and optionally write it
-- to the sync config path -- to the sync config path
local serializedConfig = local serializedConfig = serde.encode("json", { tree = syncConfigTree }, true)
serde.encode("json", { tree = syncConfigTree }, true) if options.writeToFile then
if writeToFile then
fs.writeFile(syncConfigPath, serializedConfig) fs.writeFile(syncConfigPath, serializedConfig)
end end

View file

@ -0,0 +1,33 @@
local fs = require("@lune/fs")
local serde = require("@lune/serde")
local frktest = require("../../../lune_packages/frktest")
local check = frktest.assert.check
local syncConfig = require("./sync_config")
local TEST_PROJECTS_DIRS = {
"./test-files/rojo/test-projects",
"./test-files/rojo/rojo-test/serve-tests",
}
return function(test: typeof(frktest.test))
test.suite("Generates Rojo valid sync configs", function()
for _, dir in TEST_PROJECTS_DIRS do
for _, file in fs.readDir(dir) do
local fullPath = `{dir}/{file}`
test.case(`{file}`, function()
local ok, config = syncConfig(fullPath, fs.readDir(fullPath), { writeToFile = false, force = true })
check.is_true(ok)
-- Make sure that the generated config and the real configs are similar
local generatedConfig, realConfig =
serde.decode("json", config),
serde.decode("json", fs.readFile(`{fullPath}/default.project.json`))
check.table.contains(realConfig, generatedConfig)
end)
end
end
end)
end

View file

@ -4,7 +4,7 @@ indent_type = "Spaces"
call_parentheses = "Always" call_parentheses = "Always"
indent_width = 4 indent_width = 4
column_width = 80 # column_width = 80
[sort_requires] [sort_requires]
enabled = true enabled = true

1
test-files/rojo Submodule

@ -0,0 +1 @@
Subproject commit b7d3394464e0ffb350b9c8481399cc5845c10f07