mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Update mock tests
This commit is contained in:
parent
04e28f390d
commit
b3c600acc3
13 changed files with 94 additions and 246 deletions
|
|
@ -1,12 +1,13 @@
|
|||
local typeof = typeof
|
||||
|
||||
if not game then
|
||||
script = (require :: any) "test/wrap-require"
|
||||
typeof = require "test/mock".typeof
|
||||
script = require "test/wrap-require"
|
||||
typeof = require "test/mock".typeof :: any
|
||||
end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local bind = require(script.Parent.bind)
|
||||
local _, is_action = require(script.Parent.action)()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
local warn = warn -- todo
|
||||
local warn = warn
|
||||
|
||||
if not game then
|
||||
script = (require :: any) "test/wrap-require"
|
||||
script = require "test/wrap-require"
|
||||
warn = print
|
||||
end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
local get = graph.get
|
||||
local set_effect = graph.set_effect
|
||||
local capture = graph.capture
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
local Instance = Instance
|
||||
local typeof = typeof
|
||||
|
||||
if not game then
|
||||
script = (require :: any) "test/wrap-require"
|
||||
script = require "test/wrap-require"
|
||||
Instance = require("test/mock").Instance
|
||||
typeof = require("test/mock").typeof
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
|
||||
-- todo
|
||||
local Enum = Enum
|
||||
local Color3 = Color3
|
||||
local Vector3 = Vector3
|
||||
|
||||
if not game then
|
||||
local mock = require "test/mock"
|
||||
Enum = mock.Enum :: any
|
||||
Color3 = mock.Color3 :: any
|
||||
Vector3 = mock.Vector3 :: any
|
||||
Enum = mock.Enum
|
||||
Color3 = mock.Color3
|
||||
Vector3 = mock.Vector3
|
||||
end
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
if not game then script = require "test/wrap-require" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
local create = graph.create
|
||||
local get = graph.get
|
||||
local capture_and_link = graph.capture_and_link
|
||||
|
||||
local function derive<T>(fn: () -> T): () -> T
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
if not game then script = require "test/wrap-require" end
|
||||
|
||||
local flags = require(script.Parent.flags)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
-- v0.1.0
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
if not game then script = require "test/wrap-require" end
|
||||
|
||||
local create = require(script.create)
|
||||
local source = require(script.source)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
if not game then script = require "test/wrap-require" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
if not game then script = require "test/wrap-require" end
|
||||
|
||||
--[[
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
if not game then script = require "test/wrap-require" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
local set_effect = graph.set_effect
|
||||
|
|
|
|||
|
|
@ -1,184 +0,0 @@
|
|||
--!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
|
||||
119
test/mock.luau
119
test/mock.luau
|
|
@ -1,22 +1,58 @@
|
|||
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
|
||||
contains mock Roblox API interfaces for unit testing
|
||||
|
||||
]]
|
||||
|
||||
local Signal = {} :: any do
|
||||
Signal.__index = Signal
|
||||
Signal.__type = "RBXScriptSignal"
|
||||
|
||||
local function new_connection(signal, fn)
|
||||
return {
|
||||
signal = signal,
|
||||
fn = fn,
|
||||
|
||||
Disconnect = function(self)
|
||||
local i = table.find(signal.connections, self)
|
||||
if not i then return end
|
||||
table.remove(signal.connections, i)
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
function Signal.new()
|
||||
return setmetatable({
|
||||
connections = {}
|
||||
}, Signal)
|
||||
end
|
||||
|
||||
function Signal.Connect(self, fn)
|
||||
local con = new_connection(self, fn)
|
||||
table.insert(self.connections, con)
|
||||
return con
|
||||
end
|
||||
|
||||
function Signal.fire(self, ...)
|
||||
for i = #self.connections, 1, -1 do
|
||||
self.connections[i].fn(...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local Instance = {} :: any do
|
||||
--[[
|
||||
|
||||
attempt to mimic roblox engine's method of userdata reflection
|
||||
proxy can gc independantly of actual instance data
|
||||
proxy prevents gc of actual instance data
|
||||
user code never has direct access to actual instance data, only to proxy
|
||||
internal weak map kept data -> proxy
|
||||
|
||||
]]
|
||||
|
||||
type userdata = { __USERDATA: true }
|
||||
|
||||
type ProxyMT = {
|
||||
proxy: userdata,
|
||||
data: Data,
|
||||
|
|
@ -28,9 +64,9 @@ local Instance = {} do
|
|||
name: string,
|
||||
parent: Data?,
|
||||
children: { Data },
|
||||
changed: { [string]: Signal },
|
||||
changed: { [string]: RBXScriptSignal },
|
||||
properties: { [string]: unknown },
|
||||
destroying: Signal,
|
||||
destroying: RBXScriptSignal,
|
||||
class: string,
|
||||
type: "Instance"
|
||||
}
|
||||
|
|
@ -47,8 +83,6 @@ local Instance = {} do
|
|||
return t :: T & {}
|
||||
end
|
||||
|
||||
|
||||
|
||||
local proxies = {} :: { [Data]: userdata? }
|
||||
setmetatable(proxies :: any, { __mode = "v" })
|
||||
|
||||
|
|
@ -96,7 +130,7 @@ local Instance = {} do
|
|||
end
|
||||
|
||||
if data.changed[property] then
|
||||
data.changed[property]:Fire()
|
||||
Signal.fire(data.changed[property])
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -175,17 +209,17 @@ local Instance = {} do
|
|||
|
||||
function methods.Destroy(userdata: userdata)
|
||||
local data = get_data(userdata);
|
||||
data.destroying:Fire()
|
||||
Signal.fire(data.destroying)
|
||||
data.parent = nil
|
||||
if data.changed["Parent"] then
|
||||
data.changed["Parent"]:Fire()
|
||||
Signal.fire(data.changed["Parent"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local Color3 = {} do
|
||||
function Color3.new(r, g, b): Color3
|
||||
return setmetatable({ r = r, g = g, b = b}, Color3) :: any
|
||||
local Color3 = {} :: any do
|
||||
function Color3.new(r, g, b)
|
||||
return setmetatable({ r = r, g = g, b = b}, Color3)
|
||||
end
|
||||
|
||||
function Color3.__eq(a, b)
|
||||
|
|
@ -193,9 +227,9 @@ local Color3 = {} do
|
|||
end
|
||||
end
|
||||
|
||||
local Vector3 = {} do
|
||||
function Vector3.new(x, y, z): Vector3
|
||||
return setmetatable({ x = x, y = y, z = z}, Vector3) :: any
|
||||
local Vector3 = {} :: any do
|
||||
function Vector3.new(x, y, z)
|
||||
return setmetatable({ x = x, y = y, z = z}, Vector3)
|
||||
end
|
||||
|
||||
function Vector3.__eq(a, b)
|
||||
|
|
@ -203,9 +237,9 @@ local Vector3 = {} do
|
|||
end
|
||||
end
|
||||
|
||||
local Vector2 = {} do
|
||||
function Vector2.new(x, y): Vector2
|
||||
return setmetatable({ x = x, y = y }, Vector2) :: any
|
||||
local Vector2 = {} :: any do
|
||||
function Vector2.new(x, y)
|
||||
return setmetatable({ x = x, y = y }, Vector2)
|
||||
end
|
||||
|
||||
function Vector2.__eq(a, b)
|
||||
|
|
@ -213,9 +247,9 @@ local Vector2 = {} do
|
|||
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
|
||||
local UDim2 = {} :: any do
|
||||
function UDim2.fromScale(x, y)
|
||||
return setmetatable({ x = { scale = x, offset = 0 }, y = { scale = y, offset = 0 } }, UDim2)
|
||||
end
|
||||
|
||||
function UDim2.__eq(a, b)
|
||||
|
|
@ -237,18 +271,19 @@ local Enum = {} :: any do
|
|||
end})
|
||||
end
|
||||
|
||||
local function typeof(v): string
|
||||
local typeof = function(v)
|
||||
return if Instance.is_instance(v) then "Instance"
|
||||
elseif getmetatable(v) and getmetatable(v).__type then getmetatable(v).__type
|
||||
else type(v)
|
||||
end
|
||||
end :: any
|
||||
|
||||
return {
|
||||
Instance = Instance,
|
||||
Color3 = Color3,
|
||||
Vector3 = Vector3,
|
||||
Vector2 = Vector2,
|
||||
UDim2 = UDim2,
|
||||
Enum = Enum,
|
||||
typeof = typeof
|
||||
Signal = Signal,
|
||||
Instance = Instance :: typeof(Instance),
|
||||
Color3 = Color3 :: typeof(Color3),
|
||||
Vector3 = Vector3 :: typeof(Vector3),
|
||||
Vector2 = Vector2 :: typeof(Vector2),
|
||||
UDim2 = UDim2 :: typeof(UDim2),
|
||||
Enum = Enum :: typeof(Enum),
|
||||
typeof = typeof :: typeof(typeof)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
local testkit = require("test/testkit")
|
||||
local TEST, CASE, CHECK, FINISH, SKIP = testkit.test()
|
||||
|
||||
local Signal = require "test/goodsignal"
|
||||
local mock = require "test/mock"
|
||||
|
||||
|
||||
local Instance, Vector3, Color3, Vector2, UDim2 =
|
||||
mock.Instance, mock.Vector3, mock.Color3, mock.Vector2, mock.UDim2
|
||||
local Instance, Signal = mock.Instance, mock.Signal
|
||||
|
||||
local vide = require "src/init"
|
||||
|
||||
|
|
@ -1194,7 +1191,7 @@ TEST("Events", function()
|
|||
-- testkit.print2(getmetatable(val))
|
||||
|
||||
CHECK(not connected)
|
||||
val.Value = 1; val.Signal:Fire(val.Value)
|
||||
val.Value = 1; Signal.fire(val.Signal, val.Value)
|
||||
CHECK(connected)
|
||||
end
|
||||
end)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue