Fixed issues with missing code

This commit is contained in:
Ryan Ward 2023-05-05 16:33:31 -04:00
parent 189552ac65
commit 33202260e3
6 changed files with 166 additions and 75 deletions

View File

@ -59,14 +59,19 @@ Table of contents
[Version: EventManager 0.0.1 - In The Beginning things were very different](#version-eventmanager-001---in-the-beginning-things-were-very-different) [Version: EventManager 0.0.1 - In The Beginning things were very different](#version-eventmanager-001---in-the-beginning-things-were-very-different)
# Update 16.0.0 - Getting the priorities straight # Update 16.0.0 - Getting the priorities straight
Added
--- ## Added New Integration: **priorityManager**
### New Integration - priorityManager
Allows the user to have multi auto set priorities. Also adds the functionality to create your own runners (multi:mainloop(), multi:umanager()) that you can set using the priority manager. Even if you do not have `chronos` installed these features will still work! Allows the user to have multi auto set priorities. Also adds the functionality to create your own runners (multi:mainloop(), multi:umanager()) that you can set using the priority manager. Even if you do not have `chronos` installed these features will still work!
- Allows the creation of custom priorityManagers for example:
Added
---
- multi.setClock(clock_func) -- If you have access to a clock function that works like os.clock() you can set it using this function. The priorityManager if chronos is installed sets the clock to it's current version.
- multi:setCurrentTask() -- Used to set the current processor. Used in custom processors. - multi:setCurrentTask() -- Used to set the current processor. Used in custom processors.
- multi:setCurrentProcess() -- Used to set the current processor. It should only be called on a processor object - multi:setCurrentProcess() -- Used to set the current processor. It should only be called on a processor object
- multi.success(...) -- Sends a success. Green `SUCCESS` mainly used for tests
- multi.warn(...) -- Sends a warning. Yellow `WARNING` - multi.warn(...) -- Sends a warning. Yellow `WARNING`
- multi.error(err) -- When called this function will gracefully kill multi, cleaning things up. Red `ERROR` - multi.error(err) -- When called this function will gracefully kill multi, cleaning things up. Red `ERROR`
@ -145,7 +150,7 @@ Allows the user to have multi auto set priorities. Also adds the functionality t
end) end)
function test(a,b,c) function test(a,b,c)
print("I run before all and control if things go!") print("I run before all and control if execution should continue!")
return a>b return a>b
end end
@ -246,6 +251,7 @@ Removed
Fixed Fixed
--- ---
- connections being multiplied together would block the entire connection object from pushing events! This is not the desired effect I wanted. Now only the connection reference involved in the multiplication is locked!
- multi:reallocate(processor, index) has been fixed to work with the current changes of the library. - multi:reallocate(processor, index) has been fixed to work with the current changes of the library.
- Issue with lanes not handling errors properly. This is now resolved - Issue with lanes not handling errors properly. This is now resolved
- Oversight with how pushStatus worked with nesting threaded functions, connections and forwarding events. Changes made and this works now! - Oversight with how pushStatus worked with nesting threaded functions, connections and forwarding events. Changes made and this works now!
@ -292,7 +298,7 @@ conn2:Fire()
-- Looks like this is triggering a response. It shouldn't. We need to account for this -- Looks like this is triggering a response. It shouldn't. We need to account for this
conn1:Fire() conn1:Fire()
conn1:Fire() conn1:Fire()
-- Triggering conn1 twice counted as a valid way to trigger the phantom connection (conn1 * conn2) -- Triggering conn1 twice counted as a valid way to trigger the virtual connection (conn1 * conn2)
-- Now in 15.3.1, this works properly and the above doesn't do anything. Internally connections are locked until the conditions are met. -- Now in 15.3.1, this works properly and the above doesn't do anything. Internally connections are locked until the conditions are met.
conn2:Fire() conn2:Fire()

105
init.lua
View File

@ -138,6 +138,10 @@ end
--Helpers --Helpers
function multi.setClock(c)
clock = c
end
function multi.ForEach(tab,func) function multi.ForEach(tab,func)
for i=1,#tab do func(tab[i]) end for i=1,#tab do func(tab[i]) end
end end
@ -156,10 +160,10 @@ local ignoreconn = true
local empty_func = function() end local empty_func = function() end
function multi:newConnection(protect,func,kill) function multi:newConnection(protect,func,kill)
local c={} local c={}
local call_funcs = {}
local lock = false local lock = false
local locked = {}
local fast = {} local fast = {}
c.__connectionAdded = function() end
c.rawadd = false
c.Parent = self c.Parent = self
setmetatable(c,{__call=function(self,...) setmetatable(c,{__call=function(self,...)
@ -180,6 +184,49 @@ function multi:newConnection(protect,func,kill)
return self:Connect(...) return self:Connect(...)
end end
end, end,
__mod = function(obj1, obj2) -- %
local cn = multi:newConnection()
if type(obj1) == "function" and type(obj2) == "table" then
obj2(function(...)
cn:Fire(obj1(...))
end)
else
error("Invalid mod!", type(obj1), type(obj2),"Expected function, connection(table)")
end
return cn
end,
__concat = function(obj1, obj2) -- ..
local cn = multi:newConnection()
local ref
if type(obj1) == "function" and type(obj2) == "table" then
cn(function(...)
if obj1(...) then
obj2:Fire(...)
end
end)
cn.__connectionAdded = function(conn, func)
cn:Unconnect(conn)
obj2:Connect(func)
end
elseif type(obj1) == "table" and type(obj2) == "function" then
ref = cn(function(...)
obj1:Fire(...)
obj2(...)
end)
cn.__connectionAdded = function()
cn.rawadd = true
cn:Unconnect(ref)
ref = cn(function(...)
if obj2(...) then
obj1:Fire(...)
end
end)
end
else
error("Invalid concat!", type(obj1), type(obj2),"Expected function/connection(table), connection(table)/function")
end
return cn
end,
__add = function(c1,c2) -- Or __add = function(c1,c2) -- Or
local cn = multi:newConnection() local cn = multi:newConnection()
c1(function(...) c1(function(...)
@ -192,6 +239,7 @@ function multi:newConnection(protect,func,kill)
end, end,
__mul = function(c1,c2) -- And __mul = function(c1,c2) -- And
local cn = multi:newConnection() local cn = multi:newConnection()
local ref1, ref2
if c1.__hasInstances == nil then if c1.__hasInstances == nil then
cn.__hasInstances = {2} cn.__hasInstances = {2}
cn.__count = {0} cn.__count = {0}
@ -201,25 +249,25 @@ function multi:newConnection(protect,func,kill)
cn.__count = c1.__count cn.__count = c1.__count
end end
c1(function(...) ref1 = c1(function(...)
cn.__count[1] = cn.__count[1] + 1 cn.__count[1] = cn.__count[1] + 1
c1:Lock() c1:Lock(ref1)
if cn.__count[1] == cn.__hasInstances[1] then if cn.__count[1] == cn.__hasInstances[1] then
cn:Fire(...) cn:Fire(...)
cn.__count[1] = 0 cn.__count[1] = 0
c1:Unlock() c1:Unlock(ref1)
c2:Unlock() c2:Unlock(ref2)
end end
end) end)
c2(function(...) ref2 = c2(function(...)
cn.__count[1] = cn.__count[1] + 1 cn.__count[1] = cn.__count[1] + 1
c2:Lock() c2:Lock(ref2)
if cn.__count[1] == cn.__hasInstances[1] then if cn.__count[1] == cn.__hasInstances[1] then
cn:Fire(...) cn:Fire(...)
cn.__count[1] = 0 cn.__count[1] = 0
c1:Unlock() c1:Unlock(ref1)
c2:Unlock() c2:Unlock(ref2)
end end
end) end)
return cn return cn
@ -330,6 +378,17 @@ function multi:newConnection(protect,func,kill)
end end
function c:Connect(func, name) function c:Connect(func, name)
local th
if thread.getRunningThread then
th = thread.getRunningThread()
end
if th then
local fref = func
func = function(...)
__CurrentConnectionThread = th
fref(...)
end
end
table.insert(fast, func) table.insert(fast, func)
if name then if name then
fast[name] = func fast[name] = func
@ -356,6 +415,11 @@ function multi:newConnection(protect,func,kill)
}) })
temp.ref = func temp.ref = func
temp.name = name temp.name = name
if self.rawadd then
self.rawadd = false
else
self.__connectionAdded(temp, func)
end
return temp return temp
end end
@ -376,7 +440,12 @@ function multi:newConnection(protect,func,kill)
c.HasConnections = c.hasConnections c.HasConnections = c.hasConnections
c.GetConnection = c.getConnection c.GetConnection = c.getConnection
if func then
c = c .. func
end
if not(ignoreconn) then if not(ignoreconn) then
if not self then return c end
self:create(c) self:create(c)
end end
@ -567,7 +636,7 @@ function multi:newBase(ins)
c.TID = _tid c.TID = _tid
c.Act=function() end c.Act=function() end
c.Parent=self c.Parent=self
c.creationTime = os.clock() c.creationTime = clock()
if ins then if ins then
table.insert(self.Mainloop,ins,c) table.insert(self.Mainloop,ins,c)
else else
@ -593,7 +662,7 @@ function multi:newTimer()
local count=0 local count=0
local paused=false local paused=false
function c:Start() function c:Start()
time=os.clock() time=clock()
return self return self
end end
function c:Get() function c:Get()
@ -611,7 +680,7 @@ function multi:newTimer()
end end
function c:Resume() function c:Resume()
paused=false paused=false
time=os.clock()-time time=clock()-time
return self return self
end end
self:create(c) self:create(c)
@ -1443,7 +1512,7 @@ function thread:newThread(name, func, ...)
globalThreads[c] = multi globalThreads[c] = multi
threadid = threadid + 1 threadid = threadid + 1
multi:getCurrentProcess():create(c) multi:getCurrentProcess():create(c)
c.creationTime = os.clock() c.creationTime = clock()
return c return c
end end
@ -1970,7 +2039,7 @@ function multi:enableLoadDetection()
if multi.maxSpd then return end if multi.maxSpd then return end
-- here we are going to run a quick benchMark solo -- here we are going to run a quick benchMark solo
local temp = self:newProcessor() local temp = self:newProcessor()
local t = os.clock() local t = clock()
local stop = false local stop = false
temp:benchMark(.01):OnBench(function(time,steps) temp:benchMark(.01):OnBench(function(time,steps)
stop = steps stop = steps
@ -2207,6 +2276,12 @@ function multi.error(self, err)
end end
end end
function multi.success(...)
local t = {}
for i,v in pairs({...}) do t[#t+1] = tostring(v) end
io.write("\x1b[92mSUCCESS:\x1b[0m " .. table.concat(t," ") .. "\n")
end
multi.GetType = multi.getType multi.GetType = multi.getType
multi.IsPaused = multi.isPaused multi.IsPaused = multi.isPaused
multi.IsActive = multi.isActive multi.IsActive = multi.isActive

View File

@ -133,11 +133,11 @@ local function init()
end end
function multi:isRegistredScheme(scheme) function multi:isRegistredScheme(scheme)
-- return registry[name] ~= nil
end end
function multi:getRegisteredScheme(scheme) function multi:getRegisteredScheme(scheme)
-- return registry[name].mainloop, registry[name].umanager, registry[name].condition
end end
local empty_func = function() return true end local empty_func = function() return true end
@ -163,6 +163,9 @@ local function init()
condition = condition, condition = condition,
static = options.static or false static = options.static or false
} }
multi.priorityScheme[name] = name
return true return true
end end
@ -200,6 +203,8 @@ local function init()
end end
local function init_chronos() local function init_chronos()
-- Let's implement a higher precision clock
multi.setClock(chronos.nanotime) -- This is also in .000 format. So a plug and play works.
thread:newThread("System Priority Manager", function() thread:newThread("System Priority Manager", function()
while true do while true do
thread.yield() thread.yield()

View File

@ -139,7 +139,7 @@ function multi:newSystemThreadedJobQueue(n)
end end
function multi:newSystemThreadedConnection(name) function multi:newSystemThreadedConnection(name)
local conn = multi.newConnection() local conn = multi:newConnection()
conn.init = function(self) return self end conn.init = function(self) return self end
GLOBAL[name or "_"] = conn GLOBAL[name or "_"] = conn
return conn return conn

View File

@ -20,7 +20,7 @@ end
The expected and actual should "match" (Might be impossible when playing with threads) The expected and actual should "match" (Might be impossible when playing with threads)
This will be pushed directly to the master as tests start existing. This will be pushed directly to the master as tests start existing.
]] ]]
local multi, thread = require("multi"):init{print=true}--{priority=true} local multi, thread = require("multi"):init{print=true,warn=true,error=false}--{priority=true}
local good = false local good = false
local proc = multi:newProcessor("Test") local proc = multi:newProcessor("Test")
@ -32,7 +32,7 @@ end)
runTest = thread:newFunction(function() runTest = thread:newFunction(function()
local alarms,tsteps,steps,loops,tloops,updaters,events=false,0,0,0,0,0,false local alarms,tsteps,steps,loops,tloops,updaters,events=false,0,0,0,0,0,false
print("Testing Basic Features. If this fails most other features will probably not work!") multi.print("Testing Basic Features. If this fails most other features will probably not work!")
proc:newAlarm(2):OnRing(function(a) proc:newAlarm(2):OnRing(function(a)
alarms = true alarms = true
a:Destroy() a:Destroy()
@ -62,18 +62,18 @@ runTest = thread:newFunction(function()
event.OnEvent(function(evnt) event.OnEvent(function(evnt)
evnt:Destroy() evnt:Destroy()
events = true events = true
print("Alarms: Ok") multi.success("Alarms: Ok")
print("Events: Ok") multi.success("Events: Ok")
if tsteps == 10 then print("TSteps: Ok") else print("TSteps: Bad!") end if tsteps == 10 then multi.success("TSteps: Ok") else multi.error("TSteps: Bad!") end
if steps == 10 then print("Steps: Ok") else print("Steps: Bad!") end if steps == 10 then multi.success("Steps: Ok") else multi.error("Steps: Bad!") end
if loops > 100 then print("Loops: Ok") else print("Loops: Bad!") end if loops > 100 then multi.success("Loops: Ok") else multi.error("Loops: Bad!") end
if tloops > 10 then print("TLoops: Ok") else print("TLoops: Bad!") end if tloops > 10 then multi.success("TLoops: Ok") else multi.error("TLoops: Bad!") end
if updaters > 100 then print("Updaters: Ok") else print("Updaters: Bad!") end if updaters > 100 then multi.success("Updaters: Ok") else multi.error("Updaters: Bad!") end
end) end)
thread.hold(event.OnEvent) thread.hold(event.OnEvent)
print("Starting Connection and Thread tests!") multi.print("Starting Connection and Thread tests!")
func = thread:newFunction(function(count) func = thread:newFunction(function(count)
print("Starting Status test: ",count) multi.print("Starting Status test: ",count)
local a = 0 local a = 0
while true do while true do
a = a + 1 a = a + 1
@ -88,13 +88,13 @@ runTest = thread:newFunction(function()
local ret3 = func(20) local ret3 = func(20)
local s1,s2,s3 = 0,0,0 local s1,s2,s3 = 0,0,0
ret.OnError(function(...) ret.OnError(function(...)
print("Func 1:",...) multi.error("Func 1:",...)
end) end)
ret2.OnError(function(...) ret2.OnError(function(...)
print("Func 2:",...) multi.error("Func 2:",...)
end) end)
ret3.OnError(function(...) ret3.OnError(function(...)
print("Func 3:",...) multi.error("Func 3:",...)
end) end)
ret.OnStatus(function(part,whole) ret.OnStatus(function(part,whole)
s1 = math.ceil((part/whole)*1000)/10 s1 = math.ceil((part/whole)*1000)/10
@ -107,31 +107,31 @@ runTest = thread:newFunction(function()
end) end)
ret.OnReturn(function(...) ret.OnReturn(function(...)
print("Done 1",...) multi.success("Done 1",...)
end) end)
ret2.OnReturn(function(...) ret2.OnReturn(function(...)
print("Done 2",...) multi.success("Done 2",...)
end) end)
ret3.OnReturn(function(...) ret3.OnReturn(function(...)
print("Done 3",...) multi.success("Done 3",...)
end) end)
local err, timeout = thread.hold(ret.OnReturn * ret2.OnReturn * ret3.OnReturn) local err, timeout = thread.hold(ret.OnReturn * ret2.OnReturn * ret3.OnReturn)
if s1 == 100 and s2 == 100 and s3 == 100 then if s1 == 100 and s2 == 100 and s3 == 100 then
print("Threads: All tests Ok") multi.success("Threads: All tests Ok")
else else
if s1>0 and s2>0 and s3 > 0 then if s1>0 and s2>0 and s3 > 0 then
print("Thread OnStatus: Ok") multi.success("Thread OnStatus: Ok")
else else
print("Threads OnStatus or thread.hold(conn) Error!") multi.error("Threads OnStatus or thread.hold(conn) Error!")
end end
if timeout then if timeout then
print("Connection Error!") multi.error("Connection Error!")
else else
print("Connection Test 1: Ok") multi.success("Connection Test 1: Ok")
end end
print("Connection holding Error!") multi.error("Connection holding Error!")
end end
conn1 = proc:newConnection() conn1 = proc:newConnection()
@ -160,30 +160,30 @@ runTest = thread:newFunction(function()
conn3:Fire() conn3:Fire()
if c1 and c2 and c3 and c4 then if c1 and c2 and c3 and c4 then
print("Connection Test 2: Ok") multi.success("Connection Test 2: Ok")
else else
print("Connection Test 2: Error") multi.error("Connection Test 2: Error")
end end
c3 = false c3 = false
c4 = false c4 = false
conn3:Unconnect(d) conn3:Unconnect(d)
conn3:Fire() conn3:Fire()
if c3 and not(c4) then if c3 and not(c4) then
print("Connection Test 3: Ok") multi.success("Connection Test 3: Ok")
else else
print("Connection Test 3: Error removing connection") multi.error("Connection Test 3: Error removing connection")
end end
if not love then if not love then
print("Testing pseudo threading") multi.print("Testing pseudo threading")
os.execute("lua tests/threadtests.lua p") os.execute("lua tests/threadtests.lua p")
print("Testing lanes threading") multi.print("Testing lanes threading")
os.execute("lua tests/threadtests.lua l") os.execute("lua tests/threadtests.lua l")
os.exit() os.exit()
end end
end) end)
runTest().OnError(function(...) runTest().OnError(function(...)
print("Error: Something went wrong with the test!") multi.error("Something went wrong with the test!")
print(...) print(...)
end) end)

View File

@ -1,8 +1,8 @@
package.path = "../?/init.lua;../?.lua;"..package.path package.path = "../?/init.lua;../?.lua;"..package.path
multi, thread = require("multi"):init{}--{priority=true} multi, thread = require("multi"):init{print=true,warn=true,error=false}--{priority=true}
proc = multi:newProcessor("Thread Test",true) proc = multi:newProcessor("Thread Test",true)
local LANES, LOVE, PSEUDO = 1, 2, 3 local LANES, LOVE, PSEUDO = 1, 2, 3
local env local env, we_good
if love then if love then
GLOBAL, THREAD = require("multi.integration.loveManager"):init() GLOBAL, THREAD = require("multi.integration.loveManager"):init()
@ -27,11 +27,11 @@ else
end end
end end
print("Testing THREAD.setENV() if the multi_assert is not found then there is a problem") multi.print("Testing THREAD.setENV() if the multi_assert is not found then there is a problem")
THREAD.setENV({ THREAD.setENV({
multi_assert = function(expected, actual, s) multi_assert = function(expected, actual, s)
if expected ~= actual then if expected ~= actual then
error(s .. " Expected: '".. expected .."' Actual: '".. actual .."'") multi.error(s .. " Expected: '".. expected .."' Actual: '".. actual .."'")
end end
end end
}) })
@ -47,7 +47,7 @@ multi:newThread("Scheduler Thread",function()
multi_assert("table", type(f), "Argument f is not a table!") multi_assert("table", type(f), "Argument f is not a table!")
queue:push("done") queue:push("done")
end,"Passing some args", 1, 2, 3, true, {"Table"}).OnError(function(self,err) end,"Passing some args", 1, 2, 3, true, {"Table"}).OnError(function(self,err)
print("Error:", err) multi.error(err)
os.exit() os.exit()
end) end)
@ -57,7 +57,7 @@ multi:newThread("Scheduler Thread",function()
thread.kill() thread.kill()
end end
print("Thread Spawning, THREAD namaspace in threads, global's working, and queues for passing data: Ok") multi.success("Thread Spawning, THREAD namaspace in threads, global's working, and queues for passing data: Ok")
func = THREAD:newFunction(function(a,b,c) func = THREAD:newFunction(function(a,b,c)
assert(a == 3, "First argument expected '3' got '".. a .."'!") assert(a == 3, "First argument expected '3' got '".. a .."'!")
@ -73,7 +73,7 @@ multi:newThread("Scheduler Thread",function()
assert(c == 3, "Third return was not '3'!") assert(c == 3, "Third return was not '3'!")
assert(d[1] == "a table", "Fourth return is not table, or doesn't contain 'a table'!") assert(d[1] == "a table", "Fourth return is not table, or doesn't contain 'a table'!")
print("Threaded Functions, arg passing, return passing, holding: Ok") multi.success("Threaded Functions, arg passing, return passing, holding: Ok")
test=multi:newSystemThreadedTable("YO"):init() test=multi:newSystemThreadedTable("YO"):init()
test["test1"]="tabletest" test["test1"]="tabletest"
@ -84,7 +84,7 @@ multi:newThread("Scheduler Thread",function()
THREAD.hold(function() return tab["test1"] end) THREAD.hold(function() return tab["test1"] end)
THREAD.sleep(.1) THREAD.sleep(.1)
tab["test2"] = "Whats so funny?" tab["test2"] = "Whats so funny?"
end).OnError(print) end).OnError(multi.error)
multi:newThread("test2",function() multi:newThread("test2",function()
thread.hold(function() return test["test2"] end) thread.hold(function() return test["test2"] end)
@ -96,11 +96,11 @@ multi:newThread("Scheduler Thread",function()
end,{sleep=1}) end,{sleep=1})
if val == multi.TIMEOUT then if val == multi.TIMEOUT then
print("SystemThreadedTables: Failed") multi.error("SystemThreadedTables: Failed")
os.exit() os.exit()
end end
print("SystemThreadedTables: Ok") multi.success("SystemThreadedTables: Ok")
local ready = false local ready = false
@ -123,11 +123,11 @@ multi:newThread("Scheduler Thread",function()
end,{sleep=2}) end,{sleep=2})
if val == multi.TIMEOUT then if val == multi.TIMEOUT then
print("SystemThreadedJobQueues: Failed") multi.error("SystemThreadedJobQueues: Failed")
os.exit() os.exit()
end end
print("SystemThreadedJobQueues: Ok") multi.success("SystemThreadedJobQueues: Ok")
queue2 = multi:newSystemThreadedQueue("Test_Queue2"):init() queue2 = multi:newSystemThreadedQueue("Test_Queue2"):init()
multi:newSystemThread("Test_Thread_2",function() multi:newSystemThread("Test_Thread_2",function()
@ -137,7 +137,7 @@ multi:newThread("Scheduler Thread",function()
queue2:push("Test_Thread_2") queue2:push("Test_Thread_2")
end) end)
multi:mainloop() multi:mainloop()
end).OnError(print) end).OnError(multi.error)
multi:newSystemThread("Test_Thread_3",function() multi:newSystemThread("Test_Thread_3",function()
queue2 = THREAD.waitFor("Test_Queue2"):init() queue2 = THREAD.waitFor("Test_Queue2"):init()
@ -146,7 +146,7 @@ multi:newThread("Scheduler Thread",function()
queue2:push("Test_Thread_3") queue2:push("Test_Thread_3")
end) end)
multi:mainloop() multi:mainloop()
end).OnError(print) end).OnError(multi.error)
connOut = multi:newSystemThreadedConnection("ConnectionNAMEHERE"):init() connOut = multi:newSystemThreadedConnection("ConnectionNAMEHERE"):init()
a=0 a=0
connOut(function(arg) connOut(function(arg)
@ -164,21 +164,26 @@ multi:newThread("Scheduler Thread",function()
count = count + 1 count = count + 1
end end
end end
end).OnError(print) end).OnError(multi.error)
_, err = thread.hold(function() return count == 9 end,{sleep=.3}) _, err = thread.hold(function() return count == 9 end,{sleep=.3})
if err == multi.TIMEOUT then if err == multi.TIMEOUT then
print("SystemThreadedConnections: Failed") multi.error("SystemThreadedConnections: Failed")
os.exit()
end end
print("SystemThreadedConnections: Ok") multi.success("SystemThreadedConnections: Ok")
print("Tests complete!") we_good = true
os.exit() os.exit()
end).OnError(print) end).OnError(multi.error)
multi.OnExit(function(err)
if not we_good then
multi.error("There was an error running some tests!")
else
multi.success("Tests complete!")
end
end)
multi:mainloop() multi:mainloop()