Update mock tests

This commit is contained in:
Aaron Smith 2023-08-09 17:58:17 +01:00
parent 04e28f390d
commit b3c600acc3
13 changed files with 94 additions and 246 deletions

View file

@ -1,12 +1,13 @@
local typeof = typeof
if not game then if not game then
script = (require :: any) "test/wrap-require" script = require "test/wrap-require"
typeof = require "test/mock".typeof typeof = require "test/mock".typeof :: any
end end
local graph = require(script.Parent.graph) local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T> type Node<T> = graph.Node<T>
local throw = require(script.Parent.throw)
local bind = require(script.Parent.bind) local bind = require(script.Parent.bind)
local _, is_action = require(script.Parent.action)() local _, is_action = require(script.Parent.action)()

View file

@ -1,13 +1,12 @@
local warn = warn -- todo local warn = warn
if not game then if not game then
script = (require :: any) "test/wrap-require" script = require "test/wrap-require"
warn = print warn = print
end end
local graph = require(script.Parent.graph) local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T> type Node<T> = graph.Node<T>
local get = graph.get
local set_effect = graph.set_effect local set_effect = graph.set_effect
local capture = graph.capture local capture = graph.capture

View file

@ -1,5 +1,8 @@
local Instance = Instance
local typeof = typeof
if not game then if not game then
script = (require :: any) "test/wrap-require" script = require "test/wrap-require"
Instance = require("test/mock").Instance Instance = require("test/mock").Instance
typeof = require("test/mock").typeof typeof = require("test/mock").typeof
end end

View file

@ -1,14 +1,12 @@
-- todo
local Enum = Enum local Enum = Enum
local Color3 = Color3 local Color3 = Color3
local Vector3 = Vector3 local Vector3 = Vector3
if not game then if not game then
local mock = require "test/mock" local mock = require "test/mock"
Enum = mock.Enum :: any Enum = mock.Enum
Color3 = mock.Color3 :: any Color3 = mock.Color3
Vector3 = mock.Vector3 :: any Vector3 = mock.Vector3
end end
return { return {

View file

@ -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 graph = require(script.Parent.graph)
local create = graph.create local create = graph.create
local get = graph.get
local capture_and_link = graph.capture_and_link local capture_and_link = graph.capture_and_link
local function derive<T>(fn: () -> T): () -> T local function derive<T>(fn: () -> T): () -> T

View file

@ -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) local flags = require(script.Parent.flags)

View file

@ -3,7 +3,7 @@
-- v0.1.0 -- 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 create = require(script.create)
local source = require(script.source) local source = require(script.source)

View file

@ -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 graph = require(script.Parent.graph)
type Node<T> = graph.Node<T> type Node<T> = graph.Node<T>

View file

@ -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
--[[ --[[

View file

@ -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 graph = require(script.Parent.graph)
local set_effect = graph.set_effect local set_effect = graph.set_effect

View file

@ -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

View file

@ -1,22 +1,58 @@
local Instance = {} do --[[
local Signal = require "test/goodsignal"
type Signal = Signal.Type
type userdata = { __USERDATA: true } 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 proxy to actual instance data attempt to mimic roblox engine's method of userdata reflection
proxy can gc independantly of actual instance data proxy can gc independantly of actual instance data
proxy prevents gc of actual instance data proxy prevents gc of actual instance data
luau code never has direct access to actual instance data, only to proxy user code never has direct access to actual instance data, only to proxy
internal weak map kept data -> proxy
proxy knows data
data does not know proxy
separate weak map kept data -> proxy
]] ]]
type userdata = { __USERDATA: true }
type ProxyMT = { type ProxyMT = {
proxy: userdata, proxy: userdata,
data: Data, data: Data,
@ -28,9 +64,9 @@ local Instance = {} do
name: string, name: string,
parent: Data?, parent: Data?,
children: { Data }, children: { Data },
changed: { [string]: Signal }, changed: { [string]: RBXScriptSignal },
properties: { [string]: unknown }, properties: { [string]: unknown },
destroying: Signal, destroying: RBXScriptSignal,
class: string, class: string,
type: "Instance" type: "Instance"
} }
@ -47,8 +83,6 @@ local Instance = {} do
return t :: T & {} return t :: T & {}
end end
local proxies = {} :: { [Data]: userdata? } local proxies = {} :: { [Data]: userdata? }
setmetatable(proxies :: any, { __mode = "v" }) setmetatable(proxies :: any, { __mode = "v" })
@ -96,7 +130,7 @@ local Instance = {} do
end end
if data.changed[property] then if data.changed[property] then
data.changed[property]:Fire() Signal.fire(data.changed[property])
end end
end end
@ -175,17 +209,17 @@ local Instance = {} do
function methods.Destroy(userdata: userdata) function methods.Destroy(userdata: userdata)
local data = get_data(userdata); local data = get_data(userdata);
data.destroying:Fire() Signal.fire(data.destroying)
data.parent = nil data.parent = nil
if data.changed["Parent"] then if data.changed["Parent"] then
data.changed["Parent"]:Fire() Signal.fire(data.changed["Parent"])
end end
end end
end end
local Color3 = {} do local Color3 = {} :: any do
function Color3.new(r, g, b): Color3 function Color3.new(r, g, b)
return setmetatable({ r = r, g = g, b = b}, Color3) :: any return setmetatable({ r = r, g = g, b = b}, Color3)
end end
function Color3.__eq(a, b) function Color3.__eq(a, b)
@ -193,9 +227,9 @@ local Color3 = {} do
end end
end end
local Vector3 = {} do local Vector3 = {} :: any do
function Vector3.new(x, y, z): Vector3 function Vector3.new(x, y, z)
return setmetatable({ x = x, y = y, z = z}, Vector3) :: any return setmetatable({ x = x, y = y, z = z}, Vector3)
end end
function Vector3.__eq(a, b) function Vector3.__eq(a, b)
@ -203,9 +237,9 @@ local Vector3 = {} do
end end
end end
local Vector2 = {} do local Vector2 = {} :: any do
function Vector2.new(x, y): Vector2 function Vector2.new(x, y)
return setmetatable({ x = x, y = y }, Vector2) :: any return setmetatable({ x = x, y = y }, Vector2)
end end
function Vector2.__eq(a, b) function Vector2.__eq(a, b)
@ -213,9 +247,9 @@ local Vector2 = {} do
end end
end end
local UDim2 = {} do local UDim2 = {} :: any do
function UDim2.fromScale(x, y): UDim2 function UDim2.fromScale(x, y)
return setmetatable({ x = { scale = x, offset = 0 }, y = { scale = y, offset = 0 } }, UDim2) :: any return setmetatable({ x = { scale = x, offset = 0 }, y = { scale = y, offset = 0 } }, UDim2)
end end
function UDim2.__eq(a, b) function UDim2.__eq(a, b)
@ -237,18 +271,19 @@ local Enum = {} :: any do
end}) end})
end end
local function typeof(v): string local typeof = function(v)
return if Instance.is_instance(v) then "Instance" return if Instance.is_instance(v) then "Instance"
elseif getmetatable(v) and getmetatable(v).__type then getmetatable(v).__type elseif getmetatable(v) and getmetatable(v).__type then getmetatable(v).__type
else type(v) else type(v)
end end :: any
return { return {
Instance = Instance, Signal = Signal,
Color3 = Color3, Instance = Instance :: typeof(Instance),
Vector3 = Vector3, Color3 = Color3 :: typeof(Color3),
Vector2 = Vector2, Vector3 = Vector3 :: typeof(Vector3),
UDim2 = UDim2, Vector2 = Vector2 :: typeof(Vector2),
Enum = Enum, UDim2 = UDim2 :: typeof(UDim2),
typeof = typeof Enum = Enum :: typeof(Enum),
typeof = typeof :: typeof(typeof)
} }

View file

@ -1,12 +1,9 @@
local testkit = require("test/testkit") local testkit = require("test/testkit")
local TEST, CASE, CHECK, FINISH, SKIP = testkit.test() local TEST, CASE, CHECK, FINISH, SKIP = testkit.test()
local Signal = require "test/goodsignal"
local mock = require "test/mock" local mock = require "test/mock"
local Instance, Signal = mock.Instance, mock.Signal
local Instance, Vector3, Color3, Vector2, UDim2 =
mock.Instance, mock.Vector3, mock.Color3, mock.Vector2, mock.UDim2
local vide = require "src/init" local vide = require "src/init"
@ -1194,7 +1191,7 @@ TEST("Events", function()
-- testkit.print2(getmetatable(val)) -- testkit.print2(getmetatable(val))
CHECK(not connected) CHECK(not connected)
val.Value = 1; val.Signal:Fire(val.Value) val.Value = 1; Signal.fire(val.Signal, val.Value)
CHECK(connected) CHECK(connected)
end end
end) end)