mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Initial commit
This commit is contained in:
commit
e76234feb5
52 changed files with 5235 additions and 0 deletions
155
test/benchmark.luau
Normal file
155
test/benchmark.luau
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
------------------------------------------------------------------------------------------
|
||||
-- benchmark.lua
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
local BENCH, START = require("test/testkit").getBenchmarkTools()
|
||||
|
||||
local vide = require "src/init"
|
||||
type State<T> = vide.State<T>
|
||||
|
||||
local function gc(n: number?)
|
||||
for i = 1, n or 3 do
|
||||
(collectgarbage :: any)("collect")
|
||||
end
|
||||
end
|
||||
|
||||
local N = 1e5
|
||||
|
||||
gc()
|
||||
|
||||
BENCH("Create state", function()
|
||||
local cache = table.create(N)
|
||||
local wrap = vide.wrap
|
||||
|
||||
for i = 1, START(N) do
|
||||
cache[i] = wrap(1)
|
||||
end
|
||||
end)
|
||||
|
||||
gc()
|
||||
|
||||
BENCH("Get state value", function()
|
||||
local state = vide.wrap(1)
|
||||
local unwrap = vide.unwrap
|
||||
|
||||
for i = 1, START(N) do
|
||||
unwrap(state)
|
||||
end
|
||||
end)
|
||||
|
||||
gc()
|
||||
|
||||
BENCH("Set state value", function()
|
||||
local _, set = vide.wrap(1)
|
||||
|
||||
for i = 1, START(N) do
|
||||
set(i)
|
||||
end
|
||||
end)
|
||||
|
||||
gc()
|
||||
|
||||
BENCH("Derive state", function()
|
||||
local cache = table.create(N)
|
||||
local state = vide.wrap(1)
|
||||
local derive = vide.derive
|
||||
|
||||
for i = 1, START(N) do
|
||||
cache[i] = derive(function(from)
|
||||
return from(state)
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
gc()
|
||||
|
||||
BENCH("Derive state (2)", function()
|
||||
local cache = table.create(N)
|
||||
local state = vide.wrap(1)
|
||||
local state2 = vide.wrap(2)
|
||||
local derive = vide.derive
|
||||
|
||||
for i = 1, START(N) do
|
||||
cache[i] = derive(function(from)
|
||||
return from(state) + from(state2)
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
gc()
|
||||
|
||||
BENCH("Derive state (shorthand)", function()
|
||||
local cache = table.create(N)
|
||||
local state = vide.wrap(1)
|
||||
|
||||
for i = 1, START(N) do
|
||||
cache[i] = state + 1
|
||||
end
|
||||
end)
|
||||
|
||||
gc()
|
||||
|
||||
BENCH("Derive state (shorthand 2)", function()
|
||||
local cache = table.create(N)
|
||||
local state = vide.wrap(1)
|
||||
local state2 = vide.wrap(2)
|
||||
|
||||
for i = 1, START(N) do
|
||||
cache[i] = state + state2
|
||||
end
|
||||
end)
|
||||
|
||||
gc()
|
||||
|
||||
BENCH("Derived state update", function()
|
||||
local stateA, setA = vide.wrap(1)
|
||||
local stateB = vide.derive(function(from) return from(stateA) end)
|
||||
local unwrap = vide.unwrap
|
||||
|
||||
for i = 1, START(N) do
|
||||
setA(i)
|
||||
unwrap(stateB)
|
||||
end
|
||||
end)
|
||||
|
||||
gc()
|
||||
|
||||
BENCH("Derived state update (shorthand)", function()
|
||||
local stateA, setA = vide.wrap(1)
|
||||
local stateB = stateA + 1
|
||||
local unwrap = vide.unwrap
|
||||
|
||||
for i = 1, START(N) do
|
||||
setA(i)
|
||||
unwrap(stateB)
|
||||
end
|
||||
end)
|
||||
|
||||
gc()
|
||||
|
||||
BENCH("Apply single property", function()
|
||||
local instance = vide.create("Frame") {}
|
||||
local apply = vide.apply
|
||||
|
||||
for i = 1, START(N) do
|
||||
apply(instance) {
|
||||
Name = i
|
||||
}
|
||||
end
|
||||
end)
|
||||
|
||||
gc()
|
||||
|
||||
BENCH("Bind state", function()
|
||||
local instance = vide.create("Frame") {}
|
||||
local state = vide.wrap(1)
|
||||
local apply = vide.apply
|
||||
|
||||
for i = 1, START(N) do
|
||||
apply(instance) {
|
||||
Name = state
|
||||
}
|
||||
end
|
||||
end)
|
||||
|
||||
return nil
|
||||
187
test/goodsignal.lua
Normal file
187
test/goodsignal.lua
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
-- wrapped task library for use in pure luau
|
||||
local task = { spawn = function(thread, ...)
|
||||
local ok, err = coroutine.resume(thread, ...)
|
||||
if not ok then error(err, 3) end
|
||||
end }
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
-- Batched Yield-Safe Signal Implementation --
|
||||
-- This is a Signal class which has effectively identical behavior to a --
|
||||
-- normal RBXScriptSignal, with the only difference being a couple extra --
|
||||
-- stack frames at the bottom of the stack trace when an error is thrown. --
|
||||
-- This implementation caches runner coroutines, so the ability to yield in --
|
||||
-- the signal handlers comes at minimal extra cost over a naive signal --
|
||||
-- implementation that either always or never spawns a thread. --
|
||||
-- --
|
||||
-- API: --
|
||||
-- local Signal = require(THIS MODULE) --
|
||||
-- local sig = Signal.new() --
|
||||
-- local connection = sig:Connect(function(arg1, arg2, ...) ... end) --
|
||||
-- sig:Fire(arg1, arg2, ...) --
|
||||
-- connection:Disconnect() --
|
||||
-- sig:DisconnectAll() --
|
||||
-- local arg1, arg2, ... = sig:Wait() --
|
||||
-- --
|
||||
-- Licence: --
|
||||
-- Licenced under the MIT licence. --
|
||||
-- --
|
||||
-- Authors: --
|
||||
-- stravant - July 31st, 2021 - Created the file. --
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
-- The currently idle thread to run the next handler on
|
||||
local freeRunnerThread = nil
|
||||
|
||||
-- Function which acquires the currently idle handler runner thread, runs the
|
||||
-- function fn on it, and then releases the thread, returning it to being the
|
||||
-- currently idle one.
|
||||
-- If there was a currently idle runner thread already, that's okay, that old
|
||||
-- one will just get thrown and eventually GCed.
|
||||
local function acquireRunnerThreadAndCallEventHandler(fn, ...)
|
||||
local acquiredRunnerThread = freeRunnerThread
|
||||
freeRunnerThread = nil
|
||||
fn(...)
|
||||
-- The handler finished running, this runner thread is free again.
|
||||
freeRunnerThread = acquiredRunnerThread
|
||||
end
|
||||
|
||||
-- Coroutine runner that we create coroutines of. The coroutine can be
|
||||
-- repeatedly resumed with functions to run followed by the argument to run
|
||||
-- them with.
|
||||
local function runEventHandlerInFreeThread()
|
||||
-- Note: We cannot use the initial set of arguments passed to
|
||||
-- runEventHandlerInFreeThread for a call to the handler, because those
|
||||
-- arguments would stay on the stack for the duration of the thread's
|
||||
-- existence, temporarily leaking references. Without access to raw bytecode
|
||||
-- there's no way for us to clear the "..." references from the stack.
|
||||
while true do
|
||||
acquireRunnerThreadAndCallEventHandler(coroutine.yield())
|
||||
end
|
||||
end
|
||||
|
||||
-- Connection class
|
||||
local Connection = {}
|
||||
Connection.__index = Connection
|
||||
|
||||
function Connection.new(signal, fn)
|
||||
return setmetatable({
|
||||
_connected = true,
|
||||
_signal = signal,
|
||||
_fn = fn,
|
||||
_next = false,
|
||||
}, Connection)
|
||||
end
|
||||
|
||||
function Connection:Disconnect()
|
||||
self._connected = false
|
||||
|
||||
-- Unhook the node, but DON'T clear it. That way any fire calls that are
|
||||
-- currently sitting on this node will be able to iterate forwards off of
|
||||
-- it, but any subsequent fire calls will not hit it, and it will be GCed
|
||||
-- when no more fire calls are sitting on it.
|
||||
if self._signal._handlerListHead == self then
|
||||
self._signal._handlerListHead = self._next
|
||||
else
|
||||
local prev = self._signal._handlerListHead
|
||||
while prev and prev._next ~= self do
|
||||
prev = prev._next
|
||||
end
|
||||
if prev then
|
||||
prev._next = self._next
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Make Connection strict
|
||||
setmetatable(Connection, {
|
||||
__index = function(tb, key)
|
||||
error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2)
|
||||
end,
|
||||
__newindex = function(tb, key, value)
|
||||
error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2)
|
||||
end
|
||||
})
|
||||
|
||||
-- Signal class
|
||||
local Signal = {}
|
||||
Signal.__index = Signal
|
||||
|
||||
function Signal.new()
|
||||
return setmetatable({
|
||||
_handlerListHead = false,
|
||||
}, Signal)
|
||||
end
|
||||
|
||||
function Signal:Connect(fn)
|
||||
if type(fn) ~= "function" then error(`attempt to connect non function (got { type(fn) })`, 2) end
|
||||
local connection = Connection.new(self, fn)
|
||||
if self._handlerListHead then
|
||||
connection._next = self._handlerListHead
|
||||
self._handlerListHead = connection
|
||||
else
|
||||
self._handlerListHead = connection
|
||||
end
|
||||
return connection
|
||||
end
|
||||
|
||||
-- Disconnect all handlers. Since we use a linked list it suffices to clear the
|
||||
-- reference to the head handler.
|
||||
function Signal:DisconnectAll()
|
||||
self._handlerListHead = false
|
||||
end
|
||||
|
||||
-- Signal:Fire(...) implemented by running the handler functions on the
|
||||
-- coRunnerThread, and any time the resulting thread yielded without returning
|
||||
-- to us, that means that it yielded to the Roblox scheduler and has been taken
|
||||
-- over by Roblox scheduling, meaning we have to make a new coroutine runner.
|
||||
function Signal:Fire(...)
|
||||
local item = self._handlerListHead
|
||||
while item do
|
||||
if item._connected then
|
||||
if not freeRunnerThread then
|
||||
freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
|
||||
-- Get the freeRunnerThread to the first yield
|
||||
coroutine.resume(freeRunnerThread)
|
||||
end
|
||||
task.spawn(freeRunnerThread, item._fn, ...)
|
||||
end
|
||||
item = item._next
|
||||
end
|
||||
end
|
||||
|
||||
-- Implement Signal:Wait() in terms of a temporary connection using
|
||||
-- a Signal:Connect() which disconnects itself.
|
||||
function Signal:Wait()
|
||||
local waitingCoroutine = coroutine.running()
|
||||
local cn;
|
||||
cn = self:Connect(function(...)
|
||||
cn:Disconnect()
|
||||
task.spawn(waitingCoroutine, ...)
|
||||
end)
|
||||
return coroutine.yield()
|
||||
end
|
||||
|
||||
-- Implement Signal:Once() in terms of a connection which disconnects
|
||||
-- itself before running the handler.
|
||||
function Signal:Once(fn)
|
||||
local cn;
|
||||
cn = self:Connect(function(...)
|
||||
if cn._connected then
|
||||
cn:Disconnect()
|
||||
end
|
||||
fn(...)
|
||||
end)
|
||||
return cn
|
||||
end
|
||||
|
||||
-- Make signal strict
|
||||
setmetatable(Signal, {
|
||||
__index = function(tb, key)
|
||||
error(("Attempt to get Signal::%s (not a valid member)"):format(tostring(key)), 2)
|
||||
end,
|
||||
__newindex = function(tb, key, value)
|
||||
error(("Attempt to set Signal::%s (not a valid member)"):format(tostring(key)), 2)
|
||||
end
|
||||
})
|
||||
|
||||
return Signal
|
||||
230
test/mock.luau
Normal file
230
test/mock.luau
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
local Instance = {} do
|
||||
local Signal = require "test/goodsignal"
|
||||
|
||||
type userdata = { __USERDATA__: true }
|
||||
|
||||
type Proxy = {
|
||||
_Userdata: userdata,
|
||||
_Data: Data,
|
||||
__index: any,
|
||||
__newindex: any
|
||||
}
|
||||
|
||||
type Data = {
|
||||
Name: string,
|
||||
Parent: Data?,
|
||||
Children: { Data },
|
||||
Changed: { [string]: RBXScriptSignal & { Fire: any } },
|
||||
Table: { [string]: unknown },
|
||||
Destroying: RBXScriptSignal,
|
||||
ClassName: string,
|
||||
Type: "Instance"
|
||||
}
|
||||
|
||||
--[[
|
||||
attempt to mimic roblox engine's method of userdata proxy to actual instance data
|
||||
proxy can gc independantly of actual instance data
|
||||
proxy prevents gc of actual instance data
|
||||
luau code never has direct access to actual instance data, only to proxy
|
||||
]]
|
||||
local proxies = {} :: { [Data]: userdata? }
|
||||
setmetatable(proxies :: any, { __mode = "v" })
|
||||
|
||||
local function getdata(userdata: userdata): Data
|
||||
local function getproxy(userdata: userdata): Proxy
|
||||
return getmetatable(userdata :: any)
|
||||
end
|
||||
|
||||
return getproxy(userdata)._Data
|
||||
end
|
||||
|
||||
local function isInstance(value: unknown): boolean
|
||||
local mt = getmetatable(value :: any)
|
||||
return mt and mt._Data and mt._Data.Type == "Instance"
|
||||
end
|
||||
|
||||
local methods = {}
|
||||
|
||||
local function __index(userdata: userdata, property: string): ()
|
||||
local data = getdata(userdata)
|
||||
return if methods[property] then methods[property]
|
||||
elseif property == "Name" then data.Name
|
||||
elseif property == "Parent" then data.Parent
|
||||
elseif property == "Destroying" then data.Destroying
|
||||
else data.Table[property]
|
||||
end
|
||||
|
||||
local function __newindex(userdata: userdata, property: string, value: unknown)
|
||||
local data = getdata(userdata)
|
||||
if property == "Name" then
|
||||
data.Name = value :: string
|
||||
elseif property == "Parent" then
|
||||
assert(value == nil or isInstance(value), "attempt to set non-instance as parent")
|
||||
local parent = data.Parent
|
||||
if parent then
|
||||
data.Parent = nil
|
||||
table.remove(parent.Children, table.find(parent.Children, data))
|
||||
end
|
||||
if value then
|
||||
data.Parent = getdata(value :: userdata)
|
||||
table.insert(getdata(value :: userdata).Children, data)
|
||||
end
|
||||
else
|
||||
data.Table[property] = value
|
||||
end
|
||||
|
||||
if data.Changed[property] then
|
||||
data.Changed[property]:Fire()
|
||||
end
|
||||
end
|
||||
|
||||
local function getuserdata(data: Data): userdata
|
||||
return proxies[data] or (function()
|
||||
local userdata = newproxy(true)
|
||||
local proxy = getmetatable(userdata)
|
||||
proxy._Userdata = userdata
|
||||
proxy._Data = data
|
||||
proxy.__index = __index
|
||||
proxy.__newindex = __newindex
|
||||
proxies[data] = userdata
|
||||
return userdata
|
||||
end)()
|
||||
end
|
||||
|
||||
function Instance.new(class: string): Instance
|
||||
local data = {
|
||||
Name = "UNNAMED",
|
||||
Parent = nil,
|
||||
Children = {},
|
||||
Changed = {},
|
||||
Table = {},
|
||||
ClassName = class,
|
||||
Destroying = Signal.new() :: any,
|
||||
Type = "Instance" :: "Instance"
|
||||
}
|
||||
|
||||
return getuserdata(data) :: any
|
||||
end
|
||||
|
||||
function Instance.isInstance(value: unknown): boolean
|
||||
return isInstance(value)
|
||||
end
|
||||
|
||||
function methods.Clone(userdata: userdata): userdata
|
||||
local data = getdata(userdata)
|
||||
local clone_userdata = (Instance.new("") :: any) :: userdata
|
||||
local clone_data = getdata(clone_userdata)
|
||||
|
||||
table.clear(clone_data)
|
||||
for i, v in next, data do
|
||||
clone_data[i] = type(v) == "table" and table.clone(v) or v
|
||||
end
|
||||
return clone_userdata
|
||||
end
|
||||
|
||||
function methods.FindFirstChild(userdata: userdata, target: string): userdata?
|
||||
local data = getdata(userdata)
|
||||
for _, child in data.Children do
|
||||
if child.Name == target then
|
||||
return getuserdata(child)
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function methods.GetChildren(userdata: userdata): { userdata }
|
||||
local children = getdata(userdata).Children
|
||||
local userdatas = table.create(#children)
|
||||
|
||||
for i, child in next, children do
|
||||
userdatas[i] = getuserdata(child)
|
||||
end
|
||||
|
||||
return userdatas
|
||||
end
|
||||
|
||||
function methods.GetPropertyChangedSignal(userdata: userdata, property: string): RBXScriptSignal
|
||||
local data = getdata(userdata)
|
||||
if not data.Changed[property] then
|
||||
data.Changed[property] = Signal.new() :: any
|
||||
end
|
||||
return data.Changed[property]
|
||||
end
|
||||
|
||||
function methods.Destroy(userdata: userdata)
|
||||
local data = getdata(userdata);
|
||||
(data.Destroying :: any):Fire()
|
||||
data.Parent = nil
|
||||
if data.Changed["Parent"] then
|
||||
data.Changed["Parent"]:Fire()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local Color3 = {} do
|
||||
function Color3.new(r, g, b): Color3
|
||||
return setmetatable({ r = r, g = g, b = b}, Color3) :: any
|
||||
end
|
||||
|
||||
function Color3.__eq(a, b)
|
||||
return a.r == b.r and a.g == b.g and a.b == b.b
|
||||
end
|
||||
end
|
||||
|
||||
local Vector3 = {} do
|
||||
function Vector3.new(x, y, z): Vector3
|
||||
return setmetatable({ x = x, y = y, z = z}, Vector3) :: any
|
||||
end
|
||||
|
||||
function Vector3.__eq(a, b)
|
||||
return a.x == b.x and a.y == b.y and a.z == b.z
|
||||
end
|
||||
end
|
||||
|
||||
local Vector2 = {} do
|
||||
function Vector2.new(x, y): Vector2
|
||||
return setmetatable({ x = x, y = y }, Vector2) :: any
|
||||
end
|
||||
|
||||
function Vector2.__eq(a, b)
|
||||
return a.x == b.x and a.y == b.y
|
||||
end
|
||||
end
|
||||
|
||||
local UDim2 = {} do
|
||||
function UDim2.fromScale(x, y): UDim2
|
||||
return setmetatable({ x = { scale = x, offset = 0 }, y = { scale = y, offset = 0 } }, UDim2) :: any
|
||||
end
|
||||
|
||||
function UDim2.__eq(a, b)
|
||||
return a.x.scale == b.x.scale and
|
||||
b.x.offset == b.x.offset and
|
||||
a.y.scale == b.y.scale and
|
||||
a.y.offset == b.y.offset
|
||||
end
|
||||
end
|
||||
|
||||
local Enum = {} :: any do
|
||||
setmetatable(Enum, { __index = function(self, index)
|
||||
local v = setmetatable({}, { __index = function(self, index)
|
||||
self[index] = true
|
||||
return true
|
||||
end})
|
||||
self[index] = v
|
||||
return v
|
||||
end})
|
||||
end
|
||||
|
||||
local function typeof(v)
|
||||
return Instance.isInstance(v) and "Instance" or type(v)
|
||||
end
|
||||
|
||||
return {
|
||||
Instance = Instance,
|
||||
Color3 = Color3,
|
||||
Vector3 = Vector3,
|
||||
Vector2 = Vector2,
|
||||
UDim2 = UDim2,
|
||||
Enum = Enum,
|
||||
typeof = typeof
|
||||
}
|
||||
72
test/syntax.luau
Normal file
72
test/syntax.luau
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
local vide = require "src/init"
|
||||
local wrap = vide.wrap
|
||||
local derive = vide.derive
|
||||
local map = vide.map
|
||||
local create = vide.create
|
||||
local unwrap = vide.unwrap
|
||||
local Event = vide.Event
|
||||
local Layout = vide.Layout
|
||||
|
||||
do
|
||||
local count = wrap(0)
|
||||
local count2 = derive(function(from)
|
||||
local c = from(count) * unwrap(count)
|
||||
return c ^ 2
|
||||
end)
|
||||
local x = count2.value
|
||||
|
||||
local count2b = count + 1
|
||||
local xb = unwrap(count2b)
|
||||
end
|
||||
|
||||
local count = wrap(0)
|
||||
local v = count.value
|
||||
|
||||
do
|
||||
local t = map(1, function(i)
|
||||
return ""
|
||||
end)
|
||||
|
||||
local t = map({ true }, function(i, v)
|
||||
return 1
|
||||
end)
|
||||
|
||||
local data = wrap { "" }
|
||||
|
||||
local t = map(data, function(i, v)
|
||||
return 1
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
|
||||
create("Frame") {
|
||||
|
||||
}
|
||||
|
||||
local Value, Computed, peek, OnEvent = nil :: any, nil :: any, nil :: any, nil :: any
|
||||
local New = nil :: any
|
||||
|
||||
local function Counter(props)
|
||||
local count, set = wrap(0)
|
||||
|
||||
return create("TextLabel") {
|
||||
Text = "Count: " .. count,
|
||||
|
||||
[Layout] = props[Layout],
|
||||
|
||||
[Event.Activated] = function()
|
||||
set(count + 1)
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
local function Frame(props)
|
||||
return create("Frame") {
|
||||
Position = props.Position
|
||||
}
|
||||
end
|
||||
|
||||
Frame { Position = UDim2.fromScale(0.5, 0.5) }
|
||||
|
||||
Frame { Position = positionState }
|
||||
321
test/testkit.luau
Normal file
321
test/testkit.luau
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
------------------------------------------------------------------------------------------
|
||||
-- testkit.luau
|
||||
-- v0.2.0
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
--[[
|
||||
|
||||
EXAMPLE USAGE:
|
||||
|
||||
local testkit = require "path-to-testkit"
|
||||
|
||||
local TEST, CASE, CHECK = testkit.getUnitTestTools()
|
||||
|
||||
TEST("test name", function()
|
||||
do CASE "A"
|
||||
CHECK(condition)
|
||||
end
|
||||
end)
|
||||
|
||||
local BENCH, START = testkit.getBenchmarkTools()
|
||||
|
||||
BENCH("benchmark name", function()
|
||||
local x = 0
|
||||
|
||||
for i = 1, START(1e6) do
|
||||
x += 1
|
||||
end
|
||||
end)
|
||||
]]
|
||||
|
||||
------------------------------------------------------------------------------------------
|
||||
-- Unit Testing
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
type Test = {
|
||||
name: string,
|
||||
activeCase: Case?,
|
||||
cases: { Case },
|
||||
duration: number,
|
||||
error: string?
|
||||
}
|
||||
|
||||
type Case = {
|
||||
name: string,
|
||||
result: number,
|
||||
line: number?
|
||||
}
|
||||
|
||||
local PASS = 1
|
||||
local FAIL = 2
|
||||
local NONE = 3
|
||||
local ERROR = 4
|
||||
|
||||
local activeTest: Test?
|
||||
local tests: { Test } = {}
|
||||
|
||||
local function outputTestResults(test: Test)
|
||||
print("\27[1;4m"..test.name.."\27[0m")
|
||||
for _, case in test.cases do
|
||||
print(
|
||||
"[" ..
|
||||
(if case.result == PASS then
|
||||
"\27[32;1mPASS\27[0m"
|
||||
elseif case.result == FAIL then
|
||||
"\27[31;1mFAIL:"..assert(case.line).."\27[0m"
|
||||
elseif case.result == NONE then
|
||||
"\27[33;1mNONE\27[0m"
|
||||
else
|
||||
"\27[41;1;30mERROR\27[0m")
|
||||
.. "] " .. case.name
|
||||
)
|
||||
end
|
||||
|
||||
if test.error then
|
||||
print("\27[31;1;30merror: " .. test.error .. "\27[0m")
|
||||
end
|
||||
|
||||
print ""
|
||||
end
|
||||
|
||||
local function CASE(name: string)
|
||||
assert(activeTest, "no active test")
|
||||
|
||||
local case: Case = {
|
||||
name = name,
|
||||
result = NONE
|
||||
}
|
||||
|
||||
activeTest.activeCase = case
|
||||
table.insert(activeTest.cases, case)
|
||||
end
|
||||
|
||||
local function CHECK(value: any): boolean
|
||||
assert(activeTest, "no active test")
|
||||
local activeCase = activeTest.activeCase
|
||||
|
||||
if not activeCase then
|
||||
CASE ""
|
||||
activeCase = activeTest.activeCase
|
||||
end; assert(activeCase, "no active case")
|
||||
|
||||
local result = value and PASS or FAIL
|
||||
|
||||
if activeCase.result == NONE or activeCase.result == PASS then
|
||||
activeCase.result = result
|
||||
activeCase.line = debug.info(2, "l")
|
||||
end
|
||||
|
||||
return result == PASS
|
||||
end
|
||||
|
||||
local function TEST(name: string, fn: () -> ())
|
||||
assert(not activeTest, "new test was started while a test was in progress")
|
||||
local test: Test = {
|
||||
name = name,
|
||||
cases = {},
|
||||
duration = 0
|
||||
}
|
||||
|
||||
activeTest = test
|
||||
table.insert(tests, test)
|
||||
|
||||
local start = os.clock()
|
||||
local msg: string?
|
||||
local success = xpcall(fn, function(m: string) msg = m .. debug.traceback("", 2) end)
|
||||
test.duration = os.clock() - start
|
||||
|
||||
if not test.activeCase then CASE "" end
|
||||
assert(test.activeCase, "no active case")
|
||||
|
||||
if not success then
|
||||
test.activeCase.result = ERROR
|
||||
test.error = msg
|
||||
end
|
||||
|
||||
activeTest = nil
|
||||
outputTestResults(test)
|
||||
end
|
||||
|
||||
local function FINISH(): boolean
|
||||
local success = true
|
||||
local totalCases = 0
|
||||
local passedCases = 0
|
||||
local duration = 0
|
||||
|
||||
for _, test in tests do
|
||||
duration += test.duration
|
||||
for _, case in test.cases do
|
||||
totalCases += 1
|
||||
if case.result == PASS or case.result == NONE then
|
||||
passedCases += 1
|
||||
else
|
||||
success = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
print(string.format("%d/%d test cases passed in %.3f ms.", passedCases, totalCases, duration*1e3))
|
||||
|
||||
local fails = totalCases - passedCases
|
||||
|
||||
print(string.format("\27[%d;1;30m%d fail%s\27[0m", fails > 0 and 41 or 42, fails, fails == 1 and "" or "s"))
|
||||
|
||||
return success, table.clear(tests)
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------------------
|
||||
-- Benchmarking
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
type Bench = {
|
||||
timeStart: number?,
|
||||
memStart: number?,
|
||||
iterations: number?
|
||||
}
|
||||
|
||||
local activeBench: Bench? = nil
|
||||
|
||||
function START(iter: number?): number
|
||||
local n = iter or 1
|
||||
if n < 1 then error("iteration count must be greater than 0", 2) end
|
||||
assert(activeBench, "no active benchmark")
|
||||
assert(not activeBench.timeStart, "clock was already started")
|
||||
|
||||
activeBench.iterations = n
|
||||
activeBench.memStart = gcinfo()
|
||||
activeBench.timeStart = os.clock()
|
||||
return n
|
||||
end
|
||||
|
||||
local function BENCH(name: string, fn: () -> ())
|
||||
assert(not activeBench, "cannot run benchmark, a benchmark is already in progress")
|
||||
|
||||
local bench: Bench = {}
|
||||
activeBench = bench
|
||||
|
||||
local memStart = gcinfo()
|
||||
local timeStart = os.clock()
|
||||
local msg: string?
|
||||
local success = xpcall(fn, function(m: string) msg = m .. debug.traceback("", 2) end)
|
||||
local timeStop = os.clock()
|
||||
local memStop = gcinfo()
|
||||
|
||||
if not success then
|
||||
print("[\27[41;1mERROR\27[0m] " .. name)
|
||||
print("\27[31;1m" .. "error: " .. msg :: string .. "\27[0m")
|
||||
activeBench = nil
|
||||
return
|
||||
end
|
||||
|
||||
timeStart = bench.timeStart or timeStart
|
||||
memStart = bench.memStart or memStart
|
||||
|
||||
local n = bench.iterations or 1
|
||||
local duration = timeStop - timeStart
|
||||
local allocated = memStop - memStart
|
||||
|
||||
print(string.format("[ %.3f us | %4.0f B ] %s", duration/n * 1e6, allocated/n * 1e3, name))
|
||||
|
||||
activeBench = nil
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------------------
|
||||
-- Printing
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
local function printa(v: unknown)
|
||||
type Buffer = { n: number, [number]: string }
|
||||
|
||||
-- overkill concatenationless string buffer
|
||||
local function tos(value: any, stack: number, str: Buffer)
|
||||
local TAB = " "
|
||||
local indent = table.concat(table.create(stack, TAB))
|
||||
|
||||
if type(value) == "string" then
|
||||
local n = str.n
|
||||
str[n + 1] = "\""
|
||||
str[n + 2] = value
|
||||
str[n + 3] = "\""
|
||||
str.n = n + 3
|
||||
elseif type(value) ~= "table" then
|
||||
local n = str.n
|
||||
str[n + 1] = value == nil and "nil" or tostring(value)
|
||||
str.n = n + 1
|
||||
elseif next(value) == nil then
|
||||
local n = str.n
|
||||
str[n + 1] = "{}"
|
||||
str.n = n + 1
|
||||
else
|
||||
local tabbed_indent = indent .. TAB
|
||||
|
||||
str.n += 1
|
||||
str[str.n] = "{\n"
|
||||
|
||||
local i, v = next(value, nil)
|
||||
while v ~= nil do
|
||||
local n = str.n
|
||||
str[n + 1] = tabbed_indent
|
||||
|
||||
if type(i) ~= "string" then
|
||||
str[n + 2] = "["
|
||||
str[n + 3] = tostring(i)
|
||||
str[n + 4] = "]"
|
||||
n += 4
|
||||
else
|
||||
str[n + 2] = tostring(i)
|
||||
n += 2
|
||||
end
|
||||
|
||||
str[n + 1] = " = "
|
||||
str.n = n + 1
|
||||
|
||||
tos(v, stack + 1, str)
|
||||
|
||||
i, v = next(value, i)
|
||||
|
||||
n = str.n
|
||||
str[n + 1] = v ~= nil and ",\n" or "\n"
|
||||
str.n = n + 1
|
||||
end
|
||||
|
||||
local n = str.n
|
||||
str[n + 1] = indent
|
||||
str[n + 2] = "}"
|
||||
str.n = n + 2
|
||||
end
|
||||
end
|
||||
|
||||
local str = { n = 0 }
|
||||
tos(v, 0, str)
|
||||
print(table.concat(str))
|
||||
end
|
||||
|
||||
printa "string"
|
||||
|
||||
printa(1)
|
||||
|
||||
printa {
|
||||
hello = 1,
|
||||
bye = "ok",
|
||||
|
||||
test = {
|
||||
1, 2, 3
|
||||
}
|
||||
}
|
||||
|
||||
------------------------------------------------------------------------------------------
|
||||
-- Return
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
getUnitTestTools = function()
|
||||
return TEST, CASE, CHECK, FINISH
|
||||
end,
|
||||
|
||||
getBenchmarkTools = function()
|
||||
return BENCH, START
|
||||
end,
|
||||
|
||||
printa = printa
|
||||
}
|
||||
1293
test/tests.luau
Normal file
1293
test/tests.luau
Normal file
File diff suppressed because it is too large
Load diff
8
test/wrap-require.lua
Normal file
8
test/wrap-require.lua
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
local function dir(directory: string)
|
||||
return setmetatable({} :: { [string]: any }, { __index = function(_, path) return directory .. path end })
|
||||
end
|
||||
|
||||
local script = dir "src/"
|
||||
script.Parent = dir "src/"
|
||||
|
||||
return script
|
||||
Loading…
Add table
Add a link
Reference in a new issue