From 83ab6bf97f0599ca249ce2c750fa018502c2a698 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 3 Dec 2024 14:58:15 +0000 Subject: [PATCH] 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`. --- .gitignore | 2 + .gitmodules | 3 + .luaurc | 7 ++ .lune/roblox_sync_config_generator.luau | 18 +++-- .lune/tests/init.luau | 74 +++++++++++++++++++ .lune/tests/reporter.luau | 45 ++++++++++++ pesde.toml | 5 +- src/generators/rojo/sourcemap.luau | 11 ++- src/generators/rojo/sourcemap.spec.luau | 87 +++++++++++++++++++++++ src/generators/rojo/sync_config.luau | 19 +++-- src/generators/rojo/sync_config.spec.luau | 33 +++++++++ stylua.toml | 2 +- test-files/rojo | 1 + 13 files changed, 277 insertions(+), 30 deletions(-) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .luaurc create mode 100644 .lune/tests/init.luau create mode 100644 .lune/tests/reporter.luau create mode 100644 src/generators/rojo/sourcemap.spec.luau create mode 100644 src/generators/rojo/sync_config.spec.luau create mode 160000 test-files/rojo diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6f599a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*_packages/ +pesde.lock \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d382b7a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "test-files/rojo"] + path = test-files/rojo + url = https://github.com/rojo-rbx/rojo.git diff --git a/.luaurc b/.luaurc new file mode 100644 index 0000000..2cc120f --- /dev/null +++ b/.luaurc @@ -0,0 +1,7 @@ +{ + "languageMode": "nonstrict", + "lint": { + "*": true, + "TableOperations": false + } +} \ No newline at end of file diff --git a/.lune/roblox_sync_config_generator.luau b/.lune/roblox_sync_config_generator.luau index a233ae0..2f06e3e 100644 --- a/.lune/roblox_sync_config_generator.luau +++ b/.lune/roblox_sync_config_generator.luau @@ -1,19 +1,17 @@ local function enter(fn: (args: { string }) -> number?): never - local process = require("@lune/process") - local stdio = require("@lune/stdio") + local process = require("@lune/process") + local stdio = require("@lune/stdio") - local startTime = os.clock() - local exitCode = fn(table.clone(process.args)) + local startTime = os.clock() + local exitCode = fn(table.clone(process.args)) - stdio.write( - `done in {stdio.style("dim")}{string.format("%.2fs", os.clock() - startTime)}{stdio.style("reset")}!\n` - ) + stdio.write(`done in {stdio.style("dim")}{string.format("%.2fs", os.clock() - startTime)}{stdio.style("reset")}!\n`) - return process.exit(exitCode) + return process.exit(exitCode) end 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) diff --git a/.lune/tests/init.luau b/.lune/tests/init.luau new file mode 100644 index 0000000..89cbbb1 --- /dev/null +++ b/.lune/tests/init.luau @@ -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())) \ No newline at end of file diff --git a/.lune/tests/reporter.luau b/.lune/tests/reporter.luau new file mode 100644 index 0000000..425d9c9 --- /dev/null +++ b/.lune/tests/reporter.luau @@ -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 }) \ No newline at end of file diff --git a/pesde.toml b/pesde.toml index ea6b026..dca8983 100644 --- a/pesde.toml +++ b/pesde.toml @@ -5,7 +5,7 @@ description = "Scripts and other utilities for use with pesde" authors = ["dai (https://www.daimond113.com/)", "Erica Marigold "] repository = "https://github.com/pesde-pkg/scripts" license = "MIT" -includes = ["src/**/*.luau", "README.md", "LICENSE.md"] +includes = ["src/**/*.luau", "pesde.toml", "README.md", "LICENSE.md"] [target] environment = "lune" @@ -13,3 +13,6 @@ lib = "src/init.luau" [indices] default = "https://github.com/daimond113/pesde-index" + +[dev_dependencies] +frktest = { name = "itsfrank/frktest", version = "^0.0.2" } diff --git a/src/generators/rojo/sourcemap.luau b/src/generators/rojo/sourcemap.luau index 72651f9..4402d9f 100644 --- a/src/generators/rojo/sourcemap.luau +++ b/src/generators/rojo/sourcemap.luau @@ -16,14 +16,10 @@ local PATH_ACTION_MAP: { [string]: (dir: string) -> number? } = { end, ["init.lua"] = function() - return stdio.write( - serde.encode("json", { filePaths = { "init.lua" } }, false) - ) + return stdio.write(serde.encode("json", { filePaths = { "init.lua" } }, false)) end, ["init.luau"] = function() - return stdio.write( - serde.encode("json", { filePaths = { "init.luau" } }, false) - ) + return stdio.write(serde.encode("json", { filePaths = { "init.luau" } }, false)) end, } @@ -32,9 +28,10 @@ local PATH_ACTION_MAP: { [string]: (dir: string) -> number? } = { --- ## Errors --- * The current process lacks permissions to a file +--- * Failure to spawn `rojo` command --- * Any I/O error occurs return function(packageDirectory: string?): boolean - packageDirectory = packageDirectory or process.cwd + packageDirectory = packageDirectory or process.cwd -- We go through the action mappings in order of priority and check for the -- file predicates, if present, we execute the action and report our status diff --git a/src/generators/rojo/sourcemap.spec.luau b/src/generators/rojo/sourcemap.spec.luau new file mode 100644 index 0000000..bc2bb5f --- /dev/null +++ b/src/generators/rojo/sourcemap.spec.luau @@ -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 diff --git a/src/generators/rojo/sync_config.luau b/src/generators/rojo/sync_config.luau index 9ece19e..cc6f95a 100644 --- a/src/generators/rojo/sync_config.luau +++ b/src/generators/rojo/sync_config.luau @@ -56,15 +56,13 @@ local PLATFORM_SEP = if process.os == "windows" then "\\" else "/" --- ## Errors --- * The current process lacks permissions to a file --- * Any I/O error occurs -return function( - packageDirectory: string?, - files: { string }, - writeToFile: boolean? -): (boolean, string?) +return function(packageDirectory: string?, files: { string }, options: { + writeToFile: boolean?, + force: boolean?, +}): (boolean, string?) packageDirectory = packageDirectory or process.cwd - local syncConfigPath = - `{packageDirectory}{PLATFORM_SEP}default.project.json` - if fs.isFile(syncConfigPath) then + local syncConfigPath = `{packageDirectory}{PLATFORM_SEP}default.project.json` + if fs.isFile(syncConfigPath) and not options.force then return true, nil end @@ -102,9 +100,8 @@ return function( -- Finally, we serialize the config to a JSON string and optionally write it -- to the sync config path - local serializedConfig = - serde.encode("json", { tree = syncConfigTree }, true) - if writeToFile then + local serializedConfig = serde.encode("json", { tree = syncConfigTree }, true) + if options.writeToFile then fs.writeFile(syncConfigPath, serializedConfig) end diff --git a/src/generators/rojo/sync_config.spec.luau b/src/generators/rojo/sync_config.spec.luau new file mode 100644 index 0000000..6069bb3 --- /dev/null +++ b/src/generators/rojo/sync_config.spec.luau @@ -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 diff --git a/stylua.toml b/stylua.toml index 2ab5776..8fa16f6 100644 --- a/stylua.toml +++ b/stylua.toml @@ -4,7 +4,7 @@ indent_type = "Spaces" call_parentheses = "Always" indent_width = 4 -column_width = 80 +# column_width = 80 [sort_requires] enabled = true diff --git a/test-files/rojo b/test-files/rojo new file mode 160000 index 0000000..b7d3394 --- /dev/null +++ b/test-files/rojo @@ -0,0 +1 @@ +Subproject commit b7d3394464e0ffb350b9c8481399cc5845c10f07