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
cb002f4f27
50 changed files with 4666 additions and 0 deletions
254
test/benchmark.luau
Normal file
254
test/benchmark.luau
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
------------------------------------------------------------------------------------------
|
||||
-- benchmark.lua
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
local BENCH, START = require("test/testkit").benchmark()
|
||||
|
||||
local vide = require "src/init"
|
||||
|
||||
local N = 2^18 -- 262144
|
||||
|
||||
BENCH("Create state", function()
|
||||
local cache = table.create(N)
|
||||
local source = vide.source
|
||||
|
||||
for i = 1, START(N) do
|
||||
cache[i] = source(1)
|
||||
end
|
||||
end)
|
||||
|
||||
BENCH("Get value", function()
|
||||
local state = vide.source(1)
|
||||
|
||||
for i = 1, START(N) do
|
||||
state()
|
||||
end
|
||||
end)
|
||||
|
||||
BENCH("Set value", function()
|
||||
local state = vide.source(1)
|
||||
|
||||
for i = 1, START(N) do
|
||||
state(i)
|
||||
end
|
||||
end)
|
||||
|
||||
BENCH("Derive 1 state", function()
|
||||
local cache = table.create(N)
|
||||
local state = vide.source(1)
|
||||
local derive = vide.derive
|
||||
|
||||
for i = 1, START(N) do
|
||||
cache[i] = derive(function()
|
||||
return state()
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
BENCH("Derive 4 states", function()
|
||||
local cache = table.create(N)
|
||||
local state = vide.source(1)
|
||||
local state2 = vide.source(2)
|
||||
local state3 = vide.source(3)
|
||||
local state4 = vide.source(4)
|
||||
local derive = vide.derive
|
||||
|
||||
for i = 1, START(N) do
|
||||
cache[i] = derive(function()
|
||||
return state() + state2() + state3() + state4()
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
BENCH("Set derived value", function()
|
||||
local state = vide.source(1)
|
||||
local _derived = vide.derive(state)
|
||||
|
||||
for i = 1, START(N) do
|
||||
state(i)
|
||||
end
|
||||
end)
|
||||
|
||||
BENCH("Apply 0 properties", function()
|
||||
local apply = require "src/apply"
|
||||
local instance = vide.create("Frame") {}
|
||||
|
||||
for i = 1, START(N) do
|
||||
apply(instance, {})
|
||||
end
|
||||
end)
|
||||
|
||||
BENCH("Apply 8 properties", function()
|
||||
local apply = require "src/apply"
|
||||
local instance = vide.create("Frame") {}
|
||||
|
||||
for i = 1, START(N) do
|
||||
apply(instance, {
|
||||
Name = i,
|
||||
Name2 = i,
|
||||
Name3 = i,
|
||||
Name4 = i,
|
||||
Name5 = i,
|
||||
Name6 = i,
|
||||
Name7 = i,
|
||||
Name8 = i,
|
||||
})
|
||||
end
|
||||
end)
|
||||
|
||||
BENCH("Bind state", function()
|
||||
local apply = require "src/apply"
|
||||
local instance = vide.create("Frame") {}
|
||||
local state = vide.source(1)
|
||||
|
||||
for i = 1, START(N) do
|
||||
apply(instance, {
|
||||
Name = state
|
||||
})
|
||||
end
|
||||
end)
|
||||
|
||||
BENCH("Update binding", function()
|
||||
local apply = require "src/apply"
|
||||
local instance = vide.create("Frame") {}
|
||||
local state = vide.source(1)
|
||||
|
||||
apply(instance, {
|
||||
Name = state
|
||||
})
|
||||
|
||||
for i = 1, START(N) do
|
||||
state(i)
|
||||
end
|
||||
end)
|
||||
|
||||
BENCH("indexes() no change", function()
|
||||
local data = {}
|
||||
|
||||
for i = 1, N do
|
||||
data[i] = i
|
||||
end
|
||||
|
||||
local state = vide.source(data)
|
||||
|
||||
local _list = vide.indexes(state, function(v, i)
|
||||
return {}
|
||||
end)
|
||||
|
||||
--state(state()) -- fill double buffer
|
||||
|
||||
START(N)
|
||||
|
||||
state(data)
|
||||
end)
|
||||
|
||||
BENCH("indexes() all change", function()
|
||||
local data = {}
|
||||
|
||||
for i = 1, N do
|
||||
data[i] = i
|
||||
end
|
||||
|
||||
local state = vide.source(data)
|
||||
|
||||
local _list = vide.indexes(state, function(v, i)
|
||||
return {}
|
||||
end)
|
||||
|
||||
--state(state()) -- fill double buffer
|
||||
|
||||
for i, v in data do
|
||||
data[i] = v + 1
|
||||
end
|
||||
|
||||
START(N)
|
||||
|
||||
state(data)
|
||||
end)
|
||||
|
||||
BENCH("indexes() all remove", function()
|
||||
local data = {}
|
||||
|
||||
for i = 1, N do
|
||||
data[i] = i
|
||||
end
|
||||
|
||||
local state = vide.source(data)
|
||||
|
||||
local _list = vide.indexes(state, function(v, i)
|
||||
return {}
|
||||
end)
|
||||
|
||||
table.clear(data)
|
||||
|
||||
START(N)
|
||||
|
||||
state(data)
|
||||
end)
|
||||
|
||||
BENCH("values() no change", function()
|
||||
local data = {}
|
||||
|
||||
for i = 1, N do
|
||||
data[i] = {}
|
||||
end
|
||||
|
||||
local state = vide.source(data)
|
||||
|
||||
local _list = vide.values(state, function(v, i)
|
||||
return {}
|
||||
end)
|
||||
|
||||
state(state()) -- fill double buffer
|
||||
|
||||
START(N)
|
||||
|
||||
state(data)
|
||||
end)
|
||||
|
||||
BENCH("values() all change", function()
|
||||
local data = {}
|
||||
|
||||
for i = 1, N do
|
||||
data[i] = {}
|
||||
end
|
||||
|
||||
local state = vide.source(data)
|
||||
|
||||
local _list = vide.values(state, function(v, i)
|
||||
return {}
|
||||
end)
|
||||
|
||||
state(state()) -- fill double buffer
|
||||
|
||||
for i = 1, N do
|
||||
local r = math.random(1, #data)
|
||||
data[i], data[r] = data[r], data[i]
|
||||
end
|
||||
|
||||
START(N)
|
||||
|
||||
state(data)
|
||||
end)
|
||||
|
||||
BENCH("values() all remove", function()
|
||||
local data = {}
|
||||
|
||||
for i = 1, N do
|
||||
data[i] = {}
|
||||
end
|
||||
|
||||
local state = vide.source(data)
|
||||
|
||||
local _list = vide.values(state, function(v, i)
|
||||
return {}
|
||||
end)
|
||||
|
||||
table.clear(data)
|
||||
|
||||
START(N)
|
||||
|
||||
state(data)
|
||||
end)
|
||||
|
||||
return nil
|
||||
184
test/goodsignal.luau
Normal file
184
test/goodsignal.luau
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
--!nocheck
|
||||
|
||||
-- modified 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 }
|
||||
|
||||
export type Type = RBXScriptSignal & { Fire: (Type, ...any)-> () }
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
-- 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
|
||||
|
||||
Signal.__type = "RBXScriptSignal"
|
||||
|
||||
function Signal.new(): Type
|
||||
return setmetatable({
|
||||
_handlerListHead = false,
|
||||
}, Signal) :: any
|
||||
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
|
||||
|
||||
return Signal
|
||||
254
test/mock.luau
Normal file
254
test/mock.luau
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
local Instance = {} do
|
||||
local Signal = require "test/goodsignal"
|
||||
type Signal = Signal.Type
|
||||
|
||||
type userdata = { __USERDATA: true }
|
||||
|
||||
--[[
|
||||
|
||||
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
|
||||
|
||||
proxy knows data
|
||||
data does not know proxy
|
||||
separate weak map kept data -> proxy
|
||||
|
||||
]]
|
||||
|
||||
type ProxyMT = {
|
||||
proxy: userdata,
|
||||
data: Data,
|
||||
__index: any,
|
||||
__newindex: any
|
||||
}
|
||||
|
||||
type Data = {
|
||||
name: string,
|
||||
parent: Data?,
|
||||
children: { Data },
|
||||
changed: { [string]: Signal },
|
||||
properties: { [string]: unknown },
|
||||
destroying: Signal,
|
||||
class: string,
|
||||
type: "Instance"
|
||||
}
|
||||
|
||||
local function deep_clone<T>(template: T & {}): T
|
||||
local t = table.clone(template :: {}) :: {}
|
||||
|
||||
for i, v in next, t do
|
||||
if type(v) == "table" then
|
||||
t[i] = deep_clone(v)
|
||||
end
|
||||
end
|
||||
|
||||
return t :: T & {}
|
||||
end
|
||||
|
||||
|
||||
|
||||
local proxies = {} :: { [Data]: userdata? }
|
||||
setmetatable(proxies :: any, { __mode = "v" })
|
||||
|
||||
local function get_data(userdata: userdata): Data
|
||||
local function f(userdata: userdata): ProxyMT
|
||||
return getmetatable(userdata :: any)
|
||||
end
|
||||
|
||||
return f(userdata).data
|
||||
end
|
||||
|
||||
local function is_instance(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 = get_data(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.properties[property]
|
||||
end
|
||||
|
||||
local function __newindex(userdata: userdata, property: string, value: unknown)
|
||||
local data = get_data(userdata)
|
||||
if property == "Name" then
|
||||
data.name = value :: string
|
||||
elseif property == "Parent" then
|
||||
assert(value == nil or is_instance(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 = get_data(value :: userdata)
|
||||
table.insert(get_data(value :: userdata).children, data)
|
||||
end
|
||||
else
|
||||
data.properties[property] = value
|
||||
end
|
||||
|
||||
if data.changed[property] then
|
||||
data.changed[property]:Fire()
|
||||
end
|
||||
end
|
||||
|
||||
local function get_proxy(data: Data): userdata
|
||||
return proxies[data] or (function()
|
||||
local userdata = newproxy(true)
|
||||
local proxy = getmetatable(userdata)
|
||||
proxy.proxy = 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 = {},
|
||||
properties = {},
|
||||
class = class,
|
||||
destroying = Signal.new() :: any,
|
||||
type = "Instance" :: "Instance"
|
||||
}
|
||||
|
||||
return get_proxy(data) :: any
|
||||
end
|
||||
|
||||
function Instance.is_instance(value: unknown): boolean
|
||||
return is_instance(value)
|
||||
end
|
||||
|
||||
function methods.Clone(userdata: userdata): userdata
|
||||
local data = get_data(userdata)
|
||||
local clone_userdata = (Instance.new("") :: any) :: userdata
|
||||
local clone_data = get_data(clone_userdata)
|
||||
|
||||
for i, v in next, deep_clone(data) do
|
||||
clone_data[i] = v
|
||||
end
|
||||
|
||||
return clone_userdata
|
||||
end
|
||||
|
||||
function methods.FindFirstChild(userdata: userdata, target: string): userdata?
|
||||
local data = get_data(userdata)
|
||||
for _, child in data.children do
|
||||
if child.name == target then
|
||||
return get_proxy(child)
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function methods.GetChildren(userdata: userdata): { userdata }
|
||||
local children = get_data(userdata).children
|
||||
local userdatas = table.create(#children)
|
||||
|
||||
for i, child in next, children do
|
||||
userdatas[i] = get_proxy(child)
|
||||
end
|
||||
|
||||
return userdatas
|
||||
end
|
||||
|
||||
function methods.GetPropertyChangedSignal(userdata: userdata, property: string): RBXScriptSignal
|
||||
local data = get_data(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 = get_data(userdata);
|
||||
data.destroying: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): string
|
||||
return if Instance.is_instance(v) then "Instance"
|
||||
elseif getmetatable(v) and getmetatable(v).__type then getmetatable(v).__type
|
||||
else type(v)
|
||||
end
|
||||
|
||||
return {
|
||||
Instance = Instance,
|
||||
Color3 = Color3,
|
||||
Vector3 = Vector3,
|
||||
Vector2 = Vector2,
|
||||
UDim2 = UDim2,
|
||||
Enum = Enum,
|
||||
typeof = typeof
|
||||
}
|
||||
460
test/testkit.luau
Normal file
460
test/testkit.luau
Normal file
|
|
@ -0,0 +1,460 @@
|
|||
--------------------------------------------------------------------------------
|
||||
-- testkit.luau
|
||||
-- v0.7.0
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local color = {
|
||||
white_underline = function(s: string)
|
||||
return `\27[1;4m{s}\27[0m`
|
||||
end,
|
||||
|
||||
white = function(s: string)
|
||||
return `\27[37;1m{s}\27[0m`
|
||||
end,
|
||||
|
||||
green = function(s: string)
|
||||
return `\27[32;1m{s}\27[0m`
|
||||
end,
|
||||
|
||||
red = function(s: string)
|
||||
return `\27[31;1m{s}\27[0m`
|
||||
end,
|
||||
|
||||
yellow = function(s: string)
|
||||
return `\27[33;1m{s}\27[0m`
|
||||
end,
|
||||
|
||||
red_highlight = function(s: string)
|
||||
return `\27[41;1;30m{s}\27[0m`
|
||||
end,
|
||||
|
||||
green_highlight = function(s: string)
|
||||
return `\27[42;1;30m{s}\27[0m`
|
||||
end,
|
||||
|
||||
gray = function(s: string)
|
||||
return `\27[30;1m{s}\27[0m`
|
||||
end,
|
||||
}
|
||||
|
||||
local function convert_units(unit: string, value: number): (number, string)
|
||||
local prefix_colors = {
|
||||
[3] = color.red,
|
||||
[2] = color.yellow,
|
||||
[1] = color.yellow,
|
||||
[0] = color.green,
|
||||
[-1] = color.red,
|
||||
[-2] = color.yellow,
|
||||
[-3] = color.green
|
||||
}
|
||||
|
||||
local prefixes = {
|
||||
[3] ="G",
|
||||
[2] ="M",
|
||||
[1] = "k",
|
||||
[0] = " ",
|
||||
[-1] = "m",
|
||||
[-2] = "u",
|
||||
[-3] = "n"
|
||||
}
|
||||
|
||||
local order = 0
|
||||
|
||||
while value >= 1000 do
|
||||
order += 1
|
||||
value /= 1000
|
||||
end
|
||||
|
||||
while value ~= 0 and value < 1 do
|
||||
order -= 1
|
||||
value *= 1000
|
||||
end
|
||||
|
||||
if value >= 100 then
|
||||
value = math.floor(value)
|
||||
elseif value >= 10 then
|
||||
value = math.floor(value * 1e1) / 1e1
|
||||
elseif value >= 1 then
|
||||
value = math.floor(value * 1e2) / 1e2
|
||||
end
|
||||
|
||||
return value, prefix_colors[order](prefixes[order] .. unit)
|
||||
end
|
||||
|
||||
local WALL = color.gray "│"
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Testing
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
type Test = {
|
||||
name: string,
|
||||
case: Case?,
|
||||
cases: { Case },
|
||||
duration: number,
|
||||
error: {
|
||||
message: string,
|
||||
trace: string
|
||||
}?
|
||||
}
|
||||
|
||||
type Case = {
|
||||
name: string,
|
||||
result: number,
|
||||
line: number?
|
||||
}
|
||||
|
||||
local PASS, FAIL, NONE, ERROR = 1, 2, 3, 4
|
||||
|
||||
local skip: string?
|
||||
local test: Test?
|
||||
local tests: { Test } = {}
|
||||
|
||||
local function output_test_result(test: Test)
|
||||
print(color.white(test.name))
|
||||
|
||||
for _, case in test.cases do
|
||||
local status = ({
|
||||
[PASS] = color.green "PASS",
|
||||
[FAIL] = color.red "FAIL",
|
||||
[NONE] = color.yellow "NONE",
|
||||
[ERROR] = color.red "FAIL"
|
||||
})[case.result]
|
||||
|
||||
local line = case.result == FAIL and color.red(`{case.line}:`) or ""
|
||||
|
||||
print(`{status}{WALL} {line}{color.gray(case.name)}`)
|
||||
end
|
||||
|
||||
if test.error then
|
||||
print(color.gray "error: " .. color.red(test.error.message))
|
||||
print(color.gray "trace: " .. color.red(test.error.trace))
|
||||
else
|
||||
print()
|
||||
end
|
||||
end
|
||||
|
||||
local function CASE(name: string)
|
||||
assert(test, "no active test")
|
||||
|
||||
local case = {
|
||||
name = name,
|
||||
result = NONE
|
||||
}
|
||||
|
||||
test.case = case
|
||||
table.insert(test.cases, case)
|
||||
end
|
||||
|
||||
local function CHECK<T>(value: T, stack: number?): T
|
||||
assert(test, "no active test")
|
||||
local case = test.case
|
||||
|
||||
if not case then
|
||||
CASE ""
|
||||
case = test.case
|
||||
end
|
||||
|
||||
assert(case, "no active case")
|
||||
|
||||
if case.result ~= FAIL then
|
||||
case.result = value and PASS or FAIL
|
||||
case.line = debug.info(stack and stack + 1 or 2, "l")
|
||||
end
|
||||
|
||||
return value
|
||||
end
|
||||
|
||||
local function TEST(name: string, fn: () -> ())
|
||||
if skip and name ~= skip then return end
|
||||
|
||||
local active = test
|
||||
assert(not active, "cannot start test while another test is in progress")
|
||||
|
||||
test = {
|
||||
name = name,
|
||||
cases = {},
|
||||
duration = 0
|
||||
}; assert(test)
|
||||
|
||||
table.insert(tests, test)
|
||||
|
||||
local start = os.clock()
|
||||
local err
|
||||
local success = xpcall(fn, function(m: string)
|
||||
err = { message = m, trace = debug.traceback(nil, 2) }
|
||||
end)
|
||||
test.duration = os.clock() - start
|
||||
|
||||
if not test.case then CASE "" end
|
||||
assert(test.case, "no active case")
|
||||
|
||||
if not success then
|
||||
test.case.result = ERROR
|
||||
test.error = err
|
||||
end
|
||||
|
||||
test = nil
|
||||
end
|
||||
|
||||
local function FINISH(): boolean
|
||||
local success = true
|
||||
local total_cases = 0
|
||||
local passed_cases = 0
|
||||
local duration = 0
|
||||
|
||||
for _, test in tests do
|
||||
duration += test.duration
|
||||
for _, case in test.cases do
|
||||
total_cases += 1
|
||||
if case.result == PASS or case.result == NONE then
|
||||
passed_cases += 1
|
||||
else
|
||||
success = false
|
||||
end
|
||||
end
|
||||
|
||||
output_test_result(test)
|
||||
end
|
||||
|
||||
print(color.gray(string.format(
|
||||
`{passed_cases}/{total_cases} test cases passed in %.3f ms.`,
|
||||
duration*1e3
|
||||
)))
|
||||
|
||||
local fails = total_cases - passed_cases
|
||||
|
||||
print(
|
||||
(
|
||||
fails > 0
|
||||
and color.red
|
||||
or color.green
|
||||
)(`{fails} {fails == 1 and "fail" or "fails"}`)
|
||||
)
|
||||
|
||||
return success, table.clear(tests)
|
||||
end
|
||||
|
||||
local function SKIP(name: string)
|
||||
assert(not test, "cannot skip during test")
|
||||
skip = name
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Benchmarking
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
type Bench = {
|
||||
time_start: number?,
|
||||
memory_start: number?,
|
||||
iterations: number?
|
||||
}
|
||||
|
||||
local bench: Bench?
|
||||
|
||||
function START(iter: number?): number
|
||||
local n = iter or 1
|
||||
assert(n > 0, "iterations must be greater than 0")
|
||||
assert(bench, "no active benchmark")
|
||||
assert(not bench.time_start, "clock was already started")
|
||||
|
||||
bench.iterations = n
|
||||
bench.memory_start = gcinfo()
|
||||
bench.time_start = os.clock()
|
||||
return n
|
||||
end
|
||||
|
||||
local function BENCH(name: string, fn: () -> ())
|
||||
local active = bench
|
||||
assert(not active, "a benchmark is already in progress")
|
||||
|
||||
bench = {}; assert(bench)
|
||||
|
||||
;(collectgarbage :: any)("collect")
|
||||
|
||||
local mem_start = gcinfo()
|
||||
local time_start = os.clock()
|
||||
local err_msg: string?
|
||||
|
||||
local success = xpcall(fn, function(m: string)
|
||||
err_msg = m .. debug.traceback(nil, 2)
|
||||
end)
|
||||
|
||||
local time_stop = os.clock()
|
||||
local mem_stop = gcinfo()
|
||||
|
||||
if not success then
|
||||
print(`{WALL}{color.red("ERROR")}{WALL} {name}`)
|
||||
print(color.gray(err_msg :: string))
|
||||
else
|
||||
time_start = bench.time_start or time_start
|
||||
mem_start = bench.memory_start or mem_start
|
||||
|
||||
local n = bench.iterations or 1
|
||||
local d, d_unit = convert_units("s", (time_stop - time_start) / n)
|
||||
local a, a_unit = convert_units("B", math.floor((mem_stop - mem_start) / n * 1e3))
|
||||
|
||||
local function round(x: number): string
|
||||
return x > 0 and x < 10 and (x - math.floor(x)) > 0
|
||||
and string.format("%2.1f", x)
|
||||
or string.format("%3.f", x)
|
||||
end
|
||||
|
||||
print(string.format(
|
||||
`%s %s %s %s{WALL} %s`,
|
||||
color.gray(tostring(round(d))),
|
||||
d_unit,
|
||||
color.gray(tostring(round(a))),
|
||||
a_unit,
|
||||
color.gray(name)
|
||||
))
|
||||
end
|
||||
|
||||
bench = nil
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Printing
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local function print2(v: unknown)
|
||||
type Buffer = { n: number, [number]: string }
|
||||
type Cyclic = { [{}]: true }
|
||||
|
||||
-- overkill concatenationless string buffer
|
||||
local function tos(value: any, stack: number, str: Buffer, cyclic: Cyclic)
|
||||
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 -- is table
|
||||
local tabbed_indent = indent .. TAB
|
||||
|
||||
str.n += 1
|
||||
|
||||
if cyclic[value] then
|
||||
str[str.n] = color.gray "*cyclic reference*"
|
||||
return
|
||||
else
|
||||
cyclic[value] = true
|
||||
end
|
||||
|
||||
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, cyclic)
|
||||
|
||||
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 }
|
||||
local cyclic = {}
|
||||
tos(v, 0, str, cyclic)
|
||||
print(table.concat(str))
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Equality
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local function shallow_eq(a: {}, b: {}): boolean
|
||||
if #a ~= #b then return false end
|
||||
|
||||
for i, v in next, a do
|
||||
if b[i] ~= v then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
for i, v in next, b do
|
||||
if a[i] ~= v then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function deep_eq(a: {}, b: {}): boolean
|
||||
if #a ~= #b then return false end
|
||||
|
||||
for i, v in next, a do
|
||||
if type(b[i]) == "table" and type(v) == "table" then
|
||||
if deep_eq(b[i], v) == false then return false end
|
||||
elseif b[i] ~= v then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
for i, v in next, b do
|
||||
if type(a[i]) == "table" and type(v) == "table" then
|
||||
if deep_eq(a[i], v) == false then return false end
|
||||
elseif a[i] ~= v then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Return
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
test = function()
|
||||
return TEST, CASE, CHECK, FINISH, SKIP
|
||||
end,
|
||||
|
||||
benchmark = function()
|
||||
return BENCH, START
|
||||
end,
|
||||
|
||||
print2 = print2,
|
||||
|
||||
seq = shallow_eq,
|
||||
deq = deep_eq,
|
||||
|
||||
color = color
|
||||
}
|
||||
1302
test/tests.luau
Normal file
1302
test/tests.luau
Normal file
File diff suppressed because it is too large
Load diff
8
test/wrap-require.luau
Normal file
8
test/wrap-require.luau
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