mock_datastore.luau
Luau, 81 lines from Engineering overview
--!nocheck
-- Mock DataStore + fake clock for plain-luau tests of save code (no Roblox needed).
-- local M = require("./mock_datastore")
-- local env = M.env() -- env.clock / env.time / env.wait / env.spawn / env.warn
-- local store = M.store() -- store.data[key], store.fail(msg, n), store.calls
-- local p = M.player(1) -- p.kicked holds the Kick message
-- task.wait advances the fake clock instantly and calls env.onWait(), so "another server releases the lock
-- after 3 waits" is one line and a 30 s wait costs no real time.
local M = {}
local function deep(t)
if type(t) ~= "table" then return t end
local o = {}
for k, v in pairs(t) do o[k] = deep(v) end
return o
end
M.deep = deep
function M.store()
local s = { data = {}, calls = 0, queue = {} }
-- throw `msg` on the next n calls (e.g. "502: API Services rejected request with error. HTTP 500")
function s.fail(msg, n)
for _ = 1, n or 1 do table.insert(s.queue, msg) end
end
function s:UpdateAsync(key, fn)
self.calls += 1
if #self.queue > 0 then error(table.remove(self.queue, 1), 0) end
-- real DataStores never share tables with the caller: copy in and out
local v = fn(deep(self.data[key]))
if v ~= nil then self.data[key] = deep(v) end
return deep(v)
end
function s:GetAsync(key)
self.calls += 1
if #self.queue > 0 then error(table.remove(self.queue, 1), 0) end
return deep(self.data[key])
end
function s:SetAsync(key, v)
self.calls += 1
if #self.queue > 0 then error(table.remove(self.queue, 1), 0) end
self.data[key] = deep(v)
end
return s
end
function M.env()
local e = { t = 0, epoch = 1000000, warns = {}, onWait = nil }
e.clock = function() return e.t end
e.time = function() return e.epoch + math.floor(e.t) end
e.wait = function(n)
e.t += (n or 0.03)
if e.onWait then e.onWait() end
return n
end
e.spawn = function(f, ...) return f(...) end -- run inline: deterministic order
e.warn = function(...) table.insert(e.warns, table.concat({ ... }, " ")) end
return e
end
function M.player(id)
local p = { UserId = id, Name = "Player" .. id, Parent = true, kicked = nil }
function p:Kick(msg) self.kicked = msg; self.Parent = nil end
return p
end
-- tiny assertion helper: collects failures, prints PASS/FAIL lines
function M.checker()
local c = { failed = 0 }
function c.check(name, cond)
print((cond and "PASS " or "FAIL ") .. name)
if not cond then c.failed += 1 end
end
function c.done()
if c.failed > 0 then error(c.failed .. " check(s) failed") end
print("all checks passed")
end
return c
end
return M