From dc2f2fa1e1f3c2aa9c0d7d92e78d6530c682fac9 Mon Sep 17 00:00:00 2001 From: Lucien Greathouse Date: Tue, 24 Mar 2020 14:07:27 -0700 Subject: [PATCH] Introduce new test scripts (#59) * Introduce new test scripts * Update references to test scripts * Use success/fail/total counts * Upgrade Lemur --- .github/workflows/ci.yml | 2 +- .gitignore | 12 ++++- CONTRIBUTING.md | 16 +++++-- default.project.json | 8 ++-- spec.lua | 96 --------------------------------------- test-place.project.json | 40 ++++++++++++++++ test/lemur.lua | 32 +++++++++++++ test/roblox-cli.sh | 18 ++++++++ test/runner.server.lua | 98 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 216 insertions(+), 106 deletions(-) delete mode 100644 spec.lua create mode 100644 test-place.project.json create mode 100644 test/lemur.lua create mode 100755 test/roblox-cli.sh create mode 100644 test/runner.server.lua diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6a39d2..5e3da7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: - name: Test run: | - lua -lluacov spec.lua + lua -lluacov test/lemur.lua luacheck src tests # luacov-coveralls default settings do not function on GitHub Actions. diff --git a/.gitignore b/.gitignore index 2a78ec5..1e65637 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,14 @@ +# Test places and build artifacts +/*.rbxlx +/*.rbxl +/*.rbxmx +/*.rbxm + +# LuaCov reports /luacov.* + +# MkDocs build output /site -/lua_install # Cargo build output -/target \ No newline at end of file +/target diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7aab26d..fbe7c1d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,21 +19,31 @@ To get started working on TestEZ, you'll need: * [Luacheck](https://github.com/mpeterv/luacheck) (`luarocks install luacheck`) * [LuaCov](https://keplerproject.github.io/luacov) (`luarocks install luacov`) -Once you have all of these installed, you can run `lua bin/install-dependencies.lua` script to grab a couple additional local dependencies automatically. +Make sure to clone the repository with submodules. You can do that to your existing repository with: + +```sh +git submodule update --init +``` Finally, you can run all of TestEZ's tests with: ```sh -lua spec.lua +lua test/lemur.lua ``` Or, to generate a LuaCov coverage report: ```sh -lua -lluacov spec.lua +lua -lluacov test/lemur.lua luacov ``` +If you're an engineer at Roblox, you can skip this setup and use Roblox-CLI. Make sure it's on your `PATH` and that you have a production Roblox Studio installation. You can then run: + +```sh +./test/roblox-cli.sh +``` + ## Pull Requests Before starting a pull request, open an issue about the feature or bug. This helps us prevent duplicated and wasted effort. These issues are a great place to ask for help if you run into problems! diff --git a/default.project.json b/default.project.json index 8ff491e..4062500 100644 --- a/default.project.json +++ b/default.project.json @@ -1,6 +1,6 @@ { - "name": "TestEZ", - "tree": { - "$path": "src" - } + "name": "TestEZ", + "tree": { + "$path": "src" + } } \ No newline at end of file diff --git a/spec.lua b/spec.lua deleted file mode 100644 index 95d1bd9..0000000 --- a/spec.lua +++ /dev/null @@ -1,96 +0,0 @@ -local LOAD_MODULES = { - {"src", "TestEZ"}, - {"tests", "TestEZTests"}, -} - --- This makes sure we can load Lemur and other libraries that depend on init.lua -package.path = package.path .. ";?/init.lua" - --- If this fails, make sure you've cloned all Git submodules. -local lemur = require("modules.lemur") - ---[[ - Collapses ModuleScripts named 'init' into their parent folders. - - This is the same behavior as Rojo. -]] -local function collapse(root) - local init = root:FindFirstChild("init") - if init then - init.Name = root.Name - init.Parent = root.Parent - - for _, child in ipairs(root:GetChildren()) do - child.Parent = init - end - - root:Destroy() - root = init - end - - for _, child in ipairs(root:GetChildren()) do - if child:IsA("Folder") then - collapse(child) - end - end - - return root -end - -local habitat = lemur.Habitat.new() - -local root = lemur.Instance.new("Folder") -root.Name = "Root" - -for _, module in ipairs(LOAD_MODULES) do - local container = habitat:loadFromFs(module[1]) - container.Name = module[2] - container.Parent = root -end - -root = collapse(root) - -local function findUnitTests(container, foundTests) - foundTests = foundTests or {} - - for _, child in ipairs(container:GetChildren()) do - if child:IsA("ModuleScript") then - table.insert(foundTests, child) - end - - findUnitTests(child, foundTests) - end - - return foundTests -end - --- Run all unit tests, which are located in .spec.lua files next to the source -local unitTests = findUnitTests(root.TestEZTests) -print("Running unit tests...") -local failureCount = 0 -local successCount = 0 -local errorMessages = {} - --- Unit tests are expected to load individual files relative to themselves -for _, testModule in ipairs(unitTests) do - local tests = habitat:require(testModule) - - for name, testFunction in pairs(tests) do - local success, message = pcall(testFunction) - - if success then - successCount = successCount + 1 - else - failureCount = failureCount + 1 - table.insert(errorMessages, ("Test: %s\nError: %s"):format(name, message)) - end - end -end - -print(("Unit tests: %d passed, %d failed\n"):format(successCount, failureCount)) -if failureCount > 0 then - print(table.concat(errorMessages, "\n\n")) - os.exit(1) -end - -print("All tests passed.") \ No newline at end of file diff --git a/test-place.project.json b/test-place.project.json new file mode 100644 index 0000000..6b9f504 --- /dev/null +++ b/test-place.project.json @@ -0,0 +1,40 @@ +{ + "name": "TestEZ Test Place", + "tree": { + "$className": "DataModel", + + "ReplicatedStorage": { + "$className": "ReplicatedStorage", + + "TestEZ": { + "$path": "src" + }, + + "TestEZTests": { + "$path": "tests" + } + }, + + "ServerScriptService": { + "$className": "ServerScriptService", + + "Run Tests": { + "$path": "test/runner.server.lua" + } + }, + + "HttpService": { + "$className": "HttpService", + "$properties": { + "HttpEnabled": true + } + }, + + "Players": { + "$className": "Players", + "$properties": { + "CharacterAutoLoads": false + } + } + } +} \ No newline at end of file diff --git a/test/lemur.lua b/test/lemur.lua new file mode 100644 index 0000000..6096f10 --- /dev/null +++ b/test/lemur.lua @@ -0,0 +1,32 @@ +--[[ + Loads TestEZ and all of its dependencies, then runs our test entrypoint. +]] + +-- If you add any dependencies, add them to this table so they'll be loaded! +local LOAD_MODULES = { + {"src", "TestEZ"}, + {"tests", "TestEZTests"}, +} + +-- This makes sure we can load Lemur and other libraries that depend on init.lua +package.path = package.path .. ";?/init.lua" + +-- If this fails, make sure you've cloned all Git submodules of this repo! +local lemur = require("modules.lemur") + +-- Create a virtual Roblox tree +local habitat = lemur.Habitat.new() + +-- We'll put all of our library code and dependencies here +local ReplicatedStorage = habitat.game:GetService("ReplicatedStorage") + +-- Load all of the modules specified above +for _, module in ipairs(LOAD_MODULES) do + local container = habitat:loadFromFs(module[1]) + container.Name = module[2] + container.Parent = ReplicatedStorage +end + +-- When Lemur implements a proper scheduling interface, we'll use that instead. +local runTests = habitat:loadFromFs("test/runner.server.lua") +habitat:require(runTests) \ No newline at end of file diff --git a/test/roblox-cli.sh b/test/roblox-cli.sh new file mode 100755 index 0000000..6d8d8dd --- /dev/null +++ b/test/roblox-cli.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# Usage: ./test/roblox-cli.sh + +if [ ! -z ${LOCALAPPDATA+x} ]; then + # Probably Windows, look for any Roblox installation in the default path. + + VERSIONS_FOLDER="$LOCALAPPDATA/Roblox/Versions" + INSTALL=`find "$VERSIONS_FOLDER" -maxdepth 1 -name version-* | head -1` + CONTENT="$INSTALL/content" +else + # Probably macOS, look for Roblox Studio in its default path. + + CONTENT="/Applications/RobloxStudio.App/Contents/Resources/content" +fi + +rojo build test-place.project.json -o TestPlace.rbxlx +roblox-cli run --load.place TestPlace.rbxlx --assetFolder "$CONTENT" \ No newline at end of file diff --git a/test/runner.server.lua b/test/runner.server.lua new file mode 100644 index 0000000..cafb2f5 --- /dev/null +++ b/test/runner.server.lua @@ -0,0 +1,98 @@ +--[[ + This test runner is invoked in all the environments that we want to test our + library in. + + We target Lemur, Roblox Studio, and Roblox-CLI. +]] + +-- luacheck: globals __LEMUR__ + +local isRobloxCli, ProcessService = pcall(game.GetService, game, "ProcessService") + +local function findUnitTests(container, foundTests) + foundTests = foundTests or {} + + for _, child in ipairs(container:GetChildren()) do + if child:IsA("ModuleScript") then + table.insert(foundTests, child) + end + + findUnitTests(child, foundTests) + end + + return foundTests +end + +local completed, result = xpcall(function() + local ReplicatedStorage = game:GetService("ReplicatedStorage") + + local testModules = findUnitTests(ReplicatedStorage.TestEZTests) + + local totalCount = 0 + local failureCount = 0 + local successCount = 0 + local errorMessages = {} + + for _, testModule in ipairs(testModules) do + local tests = require(testModule) + + print(string.format("%s", testModule.Name)) + + for testName, testFunction in pairs(tests) do + local success, message = pcall(testFunction) + totalCount = totalCount + 1 + + if success then + print(string.format(" [PASS] %s", testName)) + successCount = successCount + 1 + else + print(string.format(" [FAIL] %s", testName)) + failureCount = failureCount + 1 + + local logMessage = string.format("Test: %s\nError: %s", testName, message) + table.insert(errorMessages, logMessage) + end + end + end + + print() + print(string.format("%s tests run: %s passed, %s failed", totalCount, successCount, failureCount)) + + if #errorMessages > 0 then + print() + print(table.concat(errorMessages, "\n\n")) + end + + return failureCount == 0 and 0 or 1 +end, debug.traceback) + +local statusCode +local errorMessage = nil +if completed then + statusCode = result +else + statusCode = 1 + errorMessage = result +end + +if __LEMUR__ then + -- Lemur has access to normal Lua OS APIs + + if errorMessage ~= nil then + print(errorMessage) + end + os.exit(statusCode) +elseif isRobloxCli then + -- Roblox CLI has a special service to terminate the process + + if errorMessage ~= nil then + print(errorMessage) + end + ProcessService:Exit(statusCode) +else + -- In Studio, we can just throw an error to get the user's attention + + if errorMessage ~= nil then + error(errorMessage, 0) + end +end \ No newline at end of file