From 70f77385f325e53291d6b8d365e4b704301161df Mon Sep 17 00:00:00 2001 From: ernisto Date: Fri, 25 Apr 2025 15:03:14 -0300 Subject: [PATCH] test: initialize unit tests when update tool version --- .gitignore | 2 + .lune/update_tools.luau | 13 ++ bins/zap/tests/input.zap | 8 + bins/zap/tests/run.luau | 12 ++ bins/zap/tests/snapshots/client.luau | 216 +++++++++++++++++++++++++ bins/zap/tests/snapshots/server.luau | 234 +++++++++++++++++++++++++++ 6 files changed, 485 insertions(+) create mode 100644 bins/zap/tests/input.zap create mode 100644 bins/zap/tests/run.luau create mode 100644 bins/zap/tests/snapshots/client.luau create mode 100644 bins/zap/tests/snapshots/server.luau diff --git a/.gitignore b/.gitignore index ae9db4f..bba6c93 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ **/*_packages/ toolchainlib/pesde.lock + +**/tests/output/** diff --git a/.lune/update_tools.luau b/.lune/update_tools.luau index ad0d7cd..cf639db 100644 --- a/.lune/update_tools.luau +++ b/.lune/update_tools.luau @@ -79,6 +79,7 @@ local BINS_SRC_DIR = pathfs.getAbsolutePathOf(pathfs.Path.from("bins")) for _, binSrc in pathfs.readDir(BINS_SRC_DIR) do local absPath = BINS_SRC_DIR:join(binSrc) + local testsPath = absPath:join("tests/init.spec.luau") local binEntrypoint = absPath:join("init.luau") local manifestPath = absPath:join("pesde.toml") local readmePath = absPath:join("README.md") @@ -170,6 +171,18 @@ for _, binSrc in pathfs.readDir(BINS_SRC_DIR) do pathfs.writeFile(manifestPath, updatedManifest) end + + -- Check if the tool has any tests, and run them if they do + if pathfs.isFile(testsPath) then + local success, result = pcall(require, testsPath:toString()) + if not success then + warn(`Rollbacking tool {binSrc} version due failed tests:`, result) + pathfs.writeFile(manifestPath, manifestContents) + continue + end + else + warn(`Unit tests not found for {binSrc}, assuming that they pass`) + end end -- Fetch README for the tool repo, if present diff --git a/bins/zap/tests/input.zap b/bins/zap/tests/input.zap new file mode 100644 index 0000000..a75a69a --- /dev/null +++ b/bins/zap/tests/input.zap @@ -0,0 +1,8 @@ +opt server_output = "tests/output/server.luau" +opt client_output = "tests/output/client.luau" + +funct Test = { + call: Async, + args: (Foo: u8, Bar: string), + rets: enum { Success, Fail } +} diff --git a/bins/zap/tests/run.luau b/bins/zap/tests/run.luau new file mode 100644 index 0000000..f805d58 --- /dev/null +++ b/bins/zap/tests/run.luau @@ -0,0 +1,12 @@ +local toolchainlib = require("../lune_packages/core") +local process = require("@lune/process") +local fs = require("@lune/fs") + +-- not working, needing support +-- process.args = { "bins/zap/tests/input.zap" } +local success, err = pcall(require, "bins/zap/init.luau") + +assert(success, `failed to execute zap: {err}`) + +assert(fs.readFile("tests/output/client.luau") == fs.readFile("tests/snapshots/client.luau"), `invalid output`) +assert(fs.readFile("tests/output/server.luau") == fs.readFile("tests/snapshots/server.luau"), `invalid output`) diff --git a/bins/zap/tests/snapshots/client.luau b/bins/zap/tests/snapshots/client.luau new file mode 100644 index 0000000..4ac227b --- /dev/null +++ b/bins/zap/tests/snapshots/client.luau @@ -0,0 +1,216 @@ +--!native +--!optimize 2 +--!nocheck +--!nolint +--#selene: allow(unused_variable, global_usage) +-- Client generated by Zap v0.6.20 (https://github.com/red-blox/zap) +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") + +local outgoing_buff: buffer +local outgoing_used: number +local outgoing_size: number +local outgoing_inst: { Instance } +local outgoing_apos: number +local outgoing_ids: { number } + +local incoming_buff: buffer +local incoming_read: number +local incoming_inst: { Instance } +local incoming_ipos: number +local incoming_ids: { number } + +-- thanks to https://dom.rojo.space/binary.html#cframe +local CFrameSpecialCases = { + CFrame.Angles(0, 0, 0), + CFrame.Angles(math.rad(90), 0, 0), + CFrame.Angles(0, math.rad(180), math.rad(180)), + CFrame.Angles(math.rad(-90), 0, 0), + CFrame.Angles(0, math.rad(180), math.rad(90)), + CFrame.Angles(0, math.rad(90), math.rad(90)), + CFrame.Angles(0, 0, math.rad(90)), + CFrame.Angles(0, math.rad(-90), math.rad(90)), + CFrame.Angles(math.rad(-90), math.rad(-90), 0), + CFrame.Angles(0, math.rad(-90), 0), + CFrame.Angles(math.rad(90), math.rad(-90), 0), + CFrame.Angles(0, math.rad(90), math.rad(180)), + CFrame.Angles(0, math.rad(-90), math.rad(180)), + CFrame.Angles(0, math.rad(180), math.rad(0)), + CFrame.Angles(math.rad(-90), math.rad(-180), math.rad(0)), + CFrame.Angles(0, math.rad(0), math.rad(180)), + CFrame.Angles(math.rad(90), math.rad(180), math.rad(0)), + CFrame.Angles(0, math.rad(0), math.rad(-90)), + CFrame.Angles(0, math.rad(-90), math.rad(-90)), + CFrame.Angles(0, math.rad(-180), math.rad(-90)), + CFrame.Angles(0, math.rad(90), math.rad(-90)), + CFrame.Angles(math.rad(90), math.rad(90), 0), + CFrame.Angles(0, math.rad(90), 0), + CFrame.Angles(math.rad(-90), math.rad(90), 0), +} + +local function alloc(len: number) + if outgoing_used + len > outgoing_size then + while outgoing_used + len > outgoing_size do + outgoing_size = outgoing_size * 2 + end + + local new_buff = buffer.create(outgoing_size) + buffer.copy(new_buff, 0, outgoing_buff, 0, outgoing_used) + + outgoing_buff = new_buff + end + + outgoing_apos = outgoing_used + outgoing_used = outgoing_used + len + + return outgoing_apos +end + +local function read(len: number) + local pos = incoming_read + incoming_read = incoming_read + len + + return pos +end + +local function save() + return { + buff = outgoing_buff, + used = outgoing_used, + size = outgoing_size, + inst = outgoing_inst, + outgoing_ids = outgoing_ids, + incoming_ids = incoming_ids, + } +end + +local function load(data: { + buff: buffer, + used: number, + size: number, + inst: { Instance }, + outgoing_ids: { number }, + incoming_ids: { number }, +}) + outgoing_buff = data.buff + outgoing_used = data.used + outgoing_size = data.size + outgoing_inst = data.inst + outgoing_ids = data.outgoing_ids + incoming_ids = data.incoming_ids +end + +local function load_empty() + outgoing_buff = buffer.create(64) + outgoing_used = 0 + outgoing_size = 64 + outgoing_inst = {} + outgoing_ids = {} + incoming_ids = {} +end + +load_empty() + +local types = {} + +local polling_queues_reliable = {} +local polling_queues_unreliable = {} +if not RunService:IsRunning() then + local noop = function() end + return table.freeze({ + SendEvents = noop, + Test = table.freeze({ + Call = noop, + }), + }) :: Events +end +if RunService:IsServer() then + error("Cannot use the client module on the server!") +end +local remotes = ReplicatedStorage:WaitForChild("ZAP") + +local reliable = remotes:WaitForChild("ZAP_RELIABLE") +assert(reliable:IsA("RemoteEvent"), "Expected ZAP_RELIABLE to be a RemoteEvent") + +local function SendEvents() + if outgoing_used ~= 0 then + local buff = buffer.create(outgoing_used) + buffer.copy(buff, 0, outgoing_buff, 0, outgoing_used) + + reliable:FireServer(buff, outgoing_inst) + + outgoing_buff = buffer.create(64) + outgoing_used = 0 + outgoing_size = 64 + table.clear(outgoing_inst) + end +end + +RunService.Heartbeat:Connect(SendEvents) + +local reliable_events = table.create(1) +local reliable_event_queue: { [number]: { any } } = table.create(1) +local function_call_id = 0 +reliable_event_queue[0] = table.create(255) +reliable.OnClientEvent:Connect(function(buff, inst) + incoming_buff = buff + incoming_inst = inst + incoming_read = 0 + incoming_ipos = 0 + local len = buffer.len(buff) + while incoming_read < len do + local id = buffer.readu8(buff, read(1)) + if id == 0 then + local call_id = buffer.readu8(incoming_buff, read(1)) + local value + value = {} + local enum_value_1 = buffer.readu8(incoming_buff, read(1)) + if enum_value_1 == 0 then + value = "Success" + elseif enum_value_1 == 1 then + value = "Fail" + else + error("Invalid enumerator") + end + local thread = reliable_event_queue[0][call_id] + -- When using actors it's possible for multiple Zap clients to exist, but only one called the Zap remote function. + if thread then + task.spawn(thread, value) + end + reliable_event_queue[0][call_id] = nil + else + error("Unknown event id") + end + end +end) +table.freeze(polling_queues_reliable) +table.freeze(polling_queues_unreliable) + +local returns = { + SendEvents = SendEvents, + Test = { + Call = function(Foo: number, Bar: string): "Success" | "Fail" + alloc(1) + buffer.writeu8(outgoing_buff, outgoing_apos, 0) + function_call_id += 1 + function_call_id %= 256 + if reliable_event_queue[0][function_call_id] then + function_call_id -= 1 + error("Zap has more than 256 calls awaiting a response, and therefore this packet has been dropped") + end + alloc(1) + buffer.writeu8(outgoing_buff, outgoing_apos, function_call_id) + alloc(1) + buffer.writeu8(outgoing_buff, outgoing_apos, Foo) + local len_1 = #Bar + alloc(2) + buffer.writeu16(outgoing_buff, outgoing_apos, len_1) + alloc(len_1) + buffer.writestring(outgoing_buff, outgoing_apos, Bar, len_1) + reliable_event_queue[0][function_call_id] = coroutine.running() + return coroutine.yield() + end, + }, +} +type Events = typeof(returns) +return returns diff --git a/bins/zap/tests/snapshots/server.luau b/bins/zap/tests/snapshots/server.luau new file mode 100644 index 0000000..bc77f61 --- /dev/null +++ b/bins/zap/tests/snapshots/server.luau @@ -0,0 +1,234 @@ +--!native +--!optimize 2 +--!nocheck +--!nolint +--#selene: allow(unused_variable, global_usage) +-- Server generated by Zap v0.6.20 (https://github.com/red-blox/zap) +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") + +local outgoing_buff: buffer +local outgoing_used: number +local outgoing_size: number +local outgoing_inst: { Instance } +local outgoing_apos: number +local outgoing_ids: { number } + +local incoming_buff: buffer +local incoming_read: number +local incoming_inst: { Instance } +local incoming_ipos: number +local incoming_ids: { number } + +-- thanks to https://dom.rojo.space/binary.html#cframe +local CFrameSpecialCases = { + CFrame.Angles(0, 0, 0), + CFrame.Angles(math.rad(90), 0, 0), + CFrame.Angles(0, math.rad(180), math.rad(180)), + CFrame.Angles(math.rad(-90), 0, 0), + CFrame.Angles(0, math.rad(180), math.rad(90)), + CFrame.Angles(0, math.rad(90), math.rad(90)), + CFrame.Angles(0, 0, math.rad(90)), + CFrame.Angles(0, math.rad(-90), math.rad(90)), + CFrame.Angles(math.rad(-90), math.rad(-90), 0), + CFrame.Angles(0, math.rad(-90), 0), + CFrame.Angles(math.rad(90), math.rad(-90), 0), + CFrame.Angles(0, math.rad(90), math.rad(180)), + CFrame.Angles(0, math.rad(-90), math.rad(180)), + CFrame.Angles(0, math.rad(180), math.rad(0)), + CFrame.Angles(math.rad(-90), math.rad(-180), math.rad(0)), + CFrame.Angles(0, math.rad(0), math.rad(180)), + CFrame.Angles(math.rad(90), math.rad(180), math.rad(0)), + CFrame.Angles(0, math.rad(0), math.rad(-90)), + CFrame.Angles(0, math.rad(-90), math.rad(-90)), + CFrame.Angles(0, math.rad(-180), math.rad(-90)), + CFrame.Angles(0, math.rad(90), math.rad(-90)), + CFrame.Angles(math.rad(90), math.rad(90), 0), + CFrame.Angles(0, math.rad(90), 0), + CFrame.Angles(math.rad(-90), math.rad(90), 0), +} + +local function alloc(len: number) + if outgoing_used + len > outgoing_size then + while outgoing_used + len > outgoing_size do + outgoing_size = outgoing_size * 2 + end + + local new_buff = buffer.create(outgoing_size) + buffer.copy(new_buff, 0, outgoing_buff, 0, outgoing_used) + + outgoing_buff = new_buff + end + + outgoing_apos = outgoing_used + outgoing_used = outgoing_used + len + + return outgoing_apos +end + +local function read(len: number) + local pos = incoming_read + incoming_read = incoming_read + len + + return pos +end + +local function save() + return { + buff = outgoing_buff, + used = outgoing_used, + size = outgoing_size, + inst = outgoing_inst, + outgoing_ids = outgoing_ids, + incoming_ids = incoming_ids, + } +end + +local function load(data: { + buff: buffer, + used: number, + size: number, + inst: { Instance }, + outgoing_ids: { number }, + incoming_ids: { number }, +}) + outgoing_buff = data.buff + outgoing_used = data.used + outgoing_size = data.size + outgoing_inst = data.inst + outgoing_ids = data.outgoing_ids + incoming_ids = data.incoming_ids +end + +local function load_empty() + outgoing_buff = buffer.create(64) + outgoing_used = 0 + outgoing_size = 64 + outgoing_inst = {} + outgoing_ids = {} + incoming_ids = {} +end + +load_empty() + +local types = {} + +local polling_queues_reliable = {} +local polling_queues_unreliable = {} +if not RunService:IsRunning() then + local noop = function() end + return table.freeze({ + SendEvents = noop, + Test = table.freeze({ + SetCallback = noop, + }), + }) :: Events +end +local Players = game:GetService("Players") + +if RunService:IsClient() then + error("Cannot use the server module on the client!") +end + +local remotes = ReplicatedStorage:FindFirstChild("ZAP") +if remotes == nil then + remotes = Instance.new("Folder") + remotes.Name = "ZAP" + remotes.Parent = ReplicatedStorage +end + +local reliable = remotes:FindFirstChild("ZAP_RELIABLE") +if reliable == nil then + reliable = Instance.new("RemoteEvent") + reliable.Name = "ZAP_RELIABLE" + reliable.Parent = remotes +end + +local player_map = {} + +local function load_player(player: Player) + if player_map[player] then + load(player_map[player]) + else + load_empty() + end +end + +Players.PlayerRemoving:Connect(function(player) + player_map[player] = nil +end) + +local function SendEvents() + for player, outgoing in player_map do + if outgoing.used > 0 then + local buff = buffer.create(outgoing.used) + buffer.copy(buff, 0, outgoing.buff, 0, outgoing.used) + + reliable:FireClient(player, buff, outgoing.inst) + + outgoing.buff = buffer.create(64) + outgoing.used = 0 + outgoing.size = 64 + table.clear(outgoing.inst) + end + end +end + +RunService.Heartbeat:Connect(SendEvents) + +local reliable_events = table.create(1) +reliable.OnServerEvent:Connect(function(player, buff, inst) + incoming_buff = buff + incoming_inst = inst + incoming_read = 0 + incoming_ipos = 0 + local len = buffer.len(buff) + while incoming_read < len do + local id = buffer.readu8(buff, read(1)) + if id == 0 then + local call_id = buffer.readu8(buff, read(1)) + local value, value2 + value = buffer.readu8(incoming_buff, read(1)) + local len_1 = buffer.readu16(incoming_buff, read(2)) + value2 = buffer.readstring(incoming_buff, read(len_1), len_1) + if reliable_events[0] then + task.spawn(function(player_2, call_id_2, value_1, value_2) + local ret_1 = reliable_events[0](player_2, value_1, value_2) + load_player(player_2) + alloc(1) + buffer.writeu8(outgoing_buff, outgoing_apos, 0) + alloc(1) + buffer.writeu8(outgoing_buff, outgoing_apos, call_id_2) + if ret_1 == "Success" then + alloc(1) + buffer.writeu8(outgoing_buff, outgoing_apos, 0) + elseif ret_1 == "Fail" then + alloc(1) + buffer.writeu8(outgoing_buff, outgoing_apos, 1) + else + error("Invalid enumerator") + end + player_map[player_2] = save() + end, player, call_id, value, value2) + end + else + error("Unknown event id") + end + end +end) +table.freeze(polling_queues_reliable) +table.freeze(polling_queues_unreliable) + +local returns = { + SendEvents = SendEvents, + Test = { + SetCallback = function(Callback: (Player: Player, Foo: number, Bar: string) -> "Success" | "Fail"): () -> () + reliable_events[0] = Callback + return function() + reliable_events[0] = nil + end + end, + }, +} +type Events = typeof(returns) +return returns