From 5bf9d6fcd43a864ba2e62bbe8afa801e77ade9c1 Mon Sep 17 00:00:00 2001 From: ernisto Date: Thu, 24 Apr 2025 18:01:39 -0300 Subject: [PATCH 1/8] ci: automatically update tools daily --- .github/workflows/update-tools.yml | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/update-tools.yml diff --git a/.github/workflows/update-tools.yml b/.github/workflows/update-tools.yml new file mode 100644 index 0000000..baf2b0e --- /dev/null +++ b/.github/workflows/update-tools.yml @@ -0,0 +1,42 @@ +name: Daily Update Manifests + +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * *' + +jobs: + update-manifests: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup pesde + uses: lumin-org/setup-pesde@v0.4.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + cache: true + + - name: Install packages + run: pesde install + timeout-minutes: 1 # sometimes the install just hangs infinitely, so you won't want spent 12 hours of ci credit for nothing :sob: + + - name: Set up Git + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + + - name: Apply updates + run: lune run .lune/update_tools -- --yes + + - name: Commit and push changes + run: | + if [[ -n "$(git status --porcelain)" ]]; then + git add . + git commit -m "chore: daily manifest update [skip ci]" + git push + else + echo "No changes to commit." + fi \ No newline at end of file From 27f7c77760a2ce3646bd0086b463a2c770144793 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 25 Apr 2025 00:47:34 +0000 Subject: [PATCH 2/8] chore: daily manifest update [skip ci] --- bins/asphalt/README.md | 28 +++++++++++++++++++--------- bins/luau-lsp/pesde.toml | 2 +- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/bins/asphalt/README.md b/bins/asphalt/README.md index aeec782..20ec6b7 100644 --- a/bins/asphalt/README.md +++ b/bins/asphalt/README.md @@ -3,6 +3,9 @@ Asphalt is a command line tool used to upload assets to Roblox and easily reference them in code. It's a modern alternative to [Tarmac](https://github.com/Roblox/Tarmac). +> [!IMPORTANT] +> This documentation is for the pre-release of Asphalt 1.0. Older versions are no longer supported or working due to Roblox API changes. The fixes will be backported soon. + ## Features - Syncs your images, sounds, models, and animations to Roblox @@ -139,6 +142,7 @@ output_path = "src/shared" - `id`: number ## Code Generation + The formatting of code generation (such as spaces, tabs, width, and semicolons) is not guaranteed by Asphalt and may change between releases without being noted as a breaking change. Therefore, it is recommended to add Asphalt's generated files to your linter/formatter's "ignore" list. Here are instructions for the most commonly used tools: @@ -147,31 +151,37 @@ Therefore, it is recommended to add Asphalt's generated files to your linter/for - [Biome](https://biomejs.dev/guides/configure-biome/#ignore-files) - [ESLint](https://eslint.org/docs/latest/use/configure/ignore) -## API Key +## Authentication -You will need an API key to sync with Asphalt. You can specify this using the `--api-key` argument, or the `ASPHALT_API_KEY` environment variable. +Both a Cookie and a properly scoped API key are required to use Asphalt. + +Previously, only an API key was required to upload images, sounds, and models to Roblox. Unfortunately, due to recent changes in Roblox's web APIs, we can no longer acquire image IDs from Roblox without cookie authentication (while still retaining the ability to upload to groups). I'd appreciate an upvote on [my DevForum post](https://devforum.roblox.com/t/provide-a-stable-open-cloud-api-to-get-an-image-id-from-a-decal-id/3594046) which outlines the issue. + +### API Key + +You can specify this using the `--api-key` argument, or the `ASPHALT_API_KEY` environment variable. You can get one from the [Creator Dashboard](https://create.roblox.com/dashboard/credentials). The following permissions are required: - `asset:read` - `asset:write` -- `legacy-assets:manage` -## Cookie -You will need a cookie to upload animations to Roblox. This is because the Open Cloud API does not support them. It will automatically detected from the current Roblox Studio installation. Otherwise, you can specify this using the `--cookie` argument, or the `ASPHALT_COOKIE` environment variable. +Make sure that you select an appropriate IP and that your API key is under the Creator (user, or group) that you've defined in `asphalt.toml`. + +### Cookie + +Your cookie will be pulled from your `.ROBLOSECURITY` environment variable. If not present, it be automatically detected from the current Roblox Studio installation. You will probably want to [disable Session Protection](https://create.roblox.com/settings/advanced) if you are using Asphalt in an environment where your IP address changes frequently, but we don't recommend this on your main Roblox account, as it makes your account less secure. ## Animations > [!WARNING] -> This feature is experimental, and Roblox may break the API we use or change its behavior without warning. - -To upload animations, make sure you specify a cookie as noted above. +> This feature uses a private Studio API, so this feature may break without warning. Asphalt expects a single [KeyframeSequence](https://create.roblox.com/docs/reference/engine/classes/KeyframeSequence) to be saved as either a `.rbxm` or `.rbxmx` file. ## Attributions -Thank you to [Tarmac](https://github.com/Roblox/tarmac) for the alpha bleeding and nested codegen implementations, which were used in this project. +Thank you to [Tarmac](https://github.com/Roblox/tarmac) for the alpha bleeding implementation, which was used in this project. diff --git a/bins/luau-lsp/pesde.toml b/bins/luau-lsp/pesde.toml index ac01bcc..e067310 100644 --- a/bins/luau-lsp/pesde.toml +++ b/bins/luau-lsp/pesde.toml @@ -1,5 +1,5 @@ name = "pesde/luau_lsp" -version = "1.43.0" +version = "1.44.1" description = "Language Server Implementation for Luau" authors = [ "CompeyDev ", From 9c83137753e6443474d5983f524bdbad0aa55585 Mon Sep 17 00:00:00 2001 From: ernisto Date: Fri, 25 Apr 2025 13:16:59 -0300 Subject: [PATCH 3/8] ci(update-tools): publish changes to pesde registry --- .github/workflows/update-tools.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/update-tools.yml b/.github/workflows/update-tools.yml index baf2b0e..8cc0624 100644 --- a/.github/workflows/update-tools.yml +++ b/.github/workflows/update-tools.yml @@ -31,6 +31,9 @@ jobs: - name: Apply updates run: lune run .lune/update_tools -- --yes + - name: Publish updates + run: pesde publish + - name: Commit and push changes run: | if [[ -n "$(git status --porcelain)" ]]; then From 07a8d045238a8931b6d3c3a24c386763b81c1b5a Mon Sep 17 00:00:00 2001 From: ernisto Date: Fri, 25 Apr 2025 15:03:08 -0300 Subject: [PATCH 4/8] fix: dont use `oldField` as regex to replace to `newField` --- .lune/update_tools.luau | 445 ++++++++++++++++++++-------------------- 1 file changed, 224 insertions(+), 221 deletions(-) diff --git a/.lune/update_tools.luau b/.lune/update_tools.luau index 059264b..ad0d7cd 100644 --- a/.lune/update_tools.luau +++ b/.lune/update_tools.luau @@ -1,221 +1,224 @@ ---> Updates tooling bin versions and READMEs - -local serde = require("@lune/serde") -local stdio = require("@lune/stdio") -local process = require("@lune/process") -local DateTime = require("@lune/datetime") - -local pathfs = require("../lune_packages/pathfs") -local base64 = require("../lune_packages/base64") - -local manifestTypes = require("../toolchainlib/src/manifest") -local Result = require("../lune_packages/result") -local Option = require("../lune_packages/option") -type Result = Result.Result -type Option = Option.Option - -local Github = require("../toolchainlib/src/github") - -type GithubContents = { - name: string, - path: string, - sha: string, - size: number, - url: string, - html_url: string, - git_url: string, - download_url: string, - type: "file" | "dir" | "symlink", - content: string, - encoding: "base64", - _links: { - self: string, - git: string, - html: string, - }, -} - -local function isoDateToTimestamp(isoDate: string): number - return DateTime.fromIsoDate(isoDate).unixTimestamp -end - -local function stripLeadingVersion(version: string): string - local stripped = string.gsub(version, "^v", "") - return stripped -end - -local function confirmAndClear(msg: string, default: boolean?): boolean - local yes = stdio.prompt("confirm", msg, default) - stdio.write( - -- Move to the previous line, clear it, move cursor to start of line, - -- and show cursor (if hidden) - "\x1b[A\x1b[K\x1b[0G\x1b[?25h" - ) - - return yes -end - -local INFO_PREFIX = `{stdio.color("green")}{stdio.style("bold")}info{stdio.color("reset")}:` -local WARN_PREFIX = `{stdio.color("yellow")}{stdio.style("bold")}warn{stdio.color("reset")}:` -local ERROR_PREFIX = `{stdio.color("red")}{stdio.style("bold")}error{stdio.color("reset")}:` - -local function warn(...) - stdio.ewrite(`{WARN_PREFIX} {stdio.format(...)}\n`) -end - -local function error(msg: string): never - stdio.ewrite(`{ERROR_PREFIX} {msg}\n`) - return process.exit(1) -end - -local function assert(expr: boolean, msg: string) - if not expr then - error(msg) - end -end - -local START_TIME = os.clock() -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 binEntrypoint = absPath:join("init.luau") - local manifestPath = absPath:join("pesde.toml") - local readmePath = absPath:join("README.md") - - -- Make sure our constructed entrypoint and manifest paths exist - assert( - pathfs.isFile(binEntrypoint) and pathfs.isFile(manifestPath), - "Either binary entrypoint or manifest not found" - ) - - local manifestContents = pathfs.readFile(manifestPath) - local manifest: manifestTypes.PesdeManifest = serde.decode("toml", manifestContents) - local entrypointContents = pathfs.readFile(binEntrypoint) - - local repoName = string.match(entrypointContents, 'require%("./lune_packages/core"%)%("([^"]+)"') - local version = manifest.version - - -- Make sure we have a repo name and version - assert(repoName ~= nil, `Failed to get repo name for tool {binSrc}`) - - local gh = Github.new( - repoName :: string, - Option.Some({ - authToken = Option.from(process.env.GITHUB_TOKEN) :: Option, - retries = Option.None :: Option, - } :: Github.Config) :: Option - ) - local transactions = gh:queueTransactions({ "FetchReleases" }) - - -- Fetch releases, and order them by published date - local releases = transactions[1]:unwrap() :: Github.GithubReleases - table.sort(releases, function(a, b) - return isoDateToTimestamp(a.published_at) < isoDateToTimestamp(b.published_at) - end) - - -- Filter for only versions which are after the current version - local releasesAfter = {} - local versionIdx = math.huge - for idx, release in releases do - if stripLeadingVersion(release.tag_name) == version then - versionIdx = idx - continue - end - - if idx > versionIdx then - releasesAfter[release.tag_name] = release - end - end - - for newVersion, newRelease in releasesAfter do - print( - `{INFO_PREFIX} Found new tool release {stdio.style("bold")}{binSrc}{stdio.style("reset")}@{stdio.style( - "dim" - )}{newVersion}{stdio.style("reset")}` - ) - - -- HACK: To prevent messing with our existing toml ordering and formatting - -- we just replace the old version field string with the new version field - -- string - - -- the string returned by serde.encode end with newlines, so remove them to maintain compatibility with different line endings - -- Old version field string: - local oldField = string.gsub(serde.encode("toml", { version = manifest.version }), "%s+$", "") - - -- New version field string: - local newField = string.gsub(serde.encode("toml", { version = stripLeadingVersion(newVersion) }), "%s+$", "") - - local updatedManifest = string.gsub( - manifestContents, - oldField, - newField, - -- Only replace the first occurrence to be safe - 1 - ) - - local toWrite = table.find(process.args, "--yes") - or table.find(process.args, "-y") - or confirmAndClear(`Update manifest for {binSrc}?`, false) - - if toWrite then - print( - `{INFO_PREFIX} Updated manifest {stdio.style("dim")}{manifestPath:stripPrefix(pathfs.cwd)}{stdio.style( - "reset" - )}` - ) - - pathfs.writeFile(manifestPath, updatedManifest) - end - end - - -- Fetch README for the tool repo, if present - transactions = gh:queueTransactions({ - { - type = "Custom", - payload = { - method = "GET", - url = `https://api.github.com/repos/{repoName}/readme`, - }, - }, - }) - - local contentsResp = transactions[1] :: Result - - local readmeRes = contentsResp:andThen(function(resp: GithubContents) - -- If there was not an error, and the contents are base64 encoded, - -- we decode the contents and return the decoded buffer - if resp.encoding == "base64" then - -- NOTE: Github uses a special format for encoding the contents, where it - -- separates the entire file into multiple lines, and encodes each line in - -- base64 - - -- This should be done by the base64 library, but oh well - local content = string.gsub(resp.content, "%s+", "") - local decoded = base64.decode(buffer.fromstring(content)) - return Result.Ok(decoded) - end - - return Result.Err(`Unsupported encoding: {resp.encoding}`) - end) - - -- NOTE: Ideally this block should be a match, but I cannot make use of - -- control flow expressions from within a function - if readmeRes:isErr() then - warn(`Failed to fetch README from tool repo {repoName}:`, readmeRes:unwrapErr()) - continue - end - - -- Write the updated README to the tool's directory - local readmeContents = readmeRes:unwrap() - -- There used to be some issues with encoding if not deleted and recreated - pathfs.removeFile(readmePath) - pathfs.writeFile(readmePath, readmeContents) - - print( - `{INFO_PREFIX} Wrote README to {stdio.style("dim")}{readmePath:stripPrefix(pathfs.cwd)}{stdio.style("reset")}` - ) -end - -local timeElapsed = string.format("%.2fs", os.clock() - START_TIME) -print(`{INFO_PREFIX} Finished checking for tool updates in {stdio.style("dim")}{timeElapsed}{stdio.style("reset")}!`) +--> Updates tooling bin versions and READMEs + +local serde = require("@lune/serde") +local stdio = require("@lune/stdio") +local process = require("@lune/process") +local DateTime = require("@lune/datetime") + +local pathfs = require("../lune_packages/pathfs") +local base64 = require("../lune_packages/base64") + +local manifestTypes = require("../toolchainlib/src/manifest") +local Result = require("../lune_packages/result") +local Option = require("../lune_packages/option") +type Result = Result.Result +type Option = Option.Option + +local Github = require("../toolchainlib/src/github") + +type GithubContents = { + name: string, + path: string, + sha: string, + size: number, + url: string, + html_url: string, + git_url: string, + download_url: string, + type: "file" | "dir" | "symlink", + content: string, + encoding: "base64", + _links: { + self: string, + git: string, + html: string, + }, +} + +local function isoDateToTimestamp(isoDate: string): number + return DateTime.fromIsoDate(isoDate).unixTimestamp +end + +local function stripLeadingVersion(version: string): string + local stripped = string.gsub(version, "^v", "") + return stripped +end + +local function confirmAndClear(msg: string, default: boolean?): boolean + local yes = stdio.prompt("confirm", msg, default) + stdio.write( + -- Move to the previous line, clear it, move cursor to start of line, + -- and show cursor (if hidden) + "\x1b[A\x1b[K\x1b[0G\x1b[?25h" + ) + + return yes +end + +local INFO_PREFIX = `{stdio.color("green")}{stdio.style("bold")}info{stdio.color("reset")}:` +local WARN_PREFIX = `{stdio.color("yellow")}{stdio.style("bold")}warn{stdio.color("reset")}:` +local ERROR_PREFIX = `{stdio.color("red")}{stdio.style("bold")}error{stdio.color("reset")}:` + +local function warn(...) + stdio.ewrite(`{WARN_PREFIX} {stdio.format(...)}\n`) +end + +local function error(msg: string): never + stdio.ewrite(`{ERROR_PREFIX} {msg}\n`) + return process.exit(1) +end + +local function assert(expr: boolean, msg: string) + if not expr then + error(msg) + end +end + +local START_TIME = os.clock() +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 binEntrypoint = absPath:join("init.luau") + local manifestPath = absPath:join("pesde.toml") + local readmePath = absPath:join("README.md") + + -- Make sure our constructed entrypoint and manifest paths exist + assert( + pathfs.isFile(binEntrypoint) and pathfs.isFile(manifestPath), + "Either binary entrypoint or manifest not found" + ) + + local manifestContents = pathfs.readFile(manifestPath) + local manifest: manifestTypes.PesdeManifest = serde.decode("toml", manifestContents) + local entrypointContents = pathfs.readFile(binEntrypoint) + + local repoName = string.match(entrypointContents, 'require%("./lune_packages/core"%)%("([^"]+)"') + local version = manifest.version + + -- Make sure we have a repo name and version + assert(repoName ~= nil, `Failed to get repo name for tool {binSrc}`) + + local gh = Github.new( + repoName :: string, + Option.Some({ + authToken = Option.from(process.env.GITHUB_TOKEN) :: Option, + retries = Option.None :: Option, + } :: Github.Config) :: Option + ) + local transactions = gh:queueTransactions({ "FetchReleases" }) + + -- Fetch releases, and order them by published date + local releases = transactions[1]:unwrap() :: Github.GithubReleases + table.sort(releases, function(a, b) + return isoDateToTimestamp(a.published_at) < isoDateToTimestamp(b.published_at) + end) + + -- Filter for only versions which are after the current version + local releasesAfter = {} + local versionIdx = math.huge + for idx, release in releases do + if stripLeadingVersion(release.tag_name) == version then + versionIdx = idx + continue + end + + if idx > versionIdx then + releasesAfter[release.tag_name] = release + end + end + + for newVersion, newRelease in releasesAfter do + print( + `{INFO_PREFIX} Found new tool release {stdio.style("bold")}{binSrc}{stdio.style("reset")}@{stdio.style( + "dim" + )}{newVersion}{stdio.style("reset")}` + ) + + -- HACK: To prevent messing with our existing toml ordering and formatting + -- we just replace the old version field string with the new version field + -- string + + -- the string returned by serde.encode end with newlines, so remove them to maintain compatibility with different line endings + -- Old version field string: + local oldField = string.gsub(serde.encode("toml", { version = manifest.version }), "%s+$", "") + + -- New version field string: + local newField = string.gsub(serde.encode("toml", { version = stripLeadingVersion(newVersion) }), "%s+$", "") + + local updatedManifest, replaces = string.gsub( + manifestContents, + string.gsub(oldField, "[%.%+%-]", "%%%0"), + newField, + -- Only replace the first occurrence to be safe + 1 + ) + if replaces == 0 then + error("failed to replace version field in manifest") + end + + local toWrite = table.find(process.args, "--yes") + or table.find(process.args, "-y") + or confirmAndClear(`Update manifest for {binSrc}?`, false) + + if toWrite then + print( + `{INFO_PREFIX} Updated manifest {stdio.style("dim")}{manifestPath:stripPrefix(pathfs.cwd)}{stdio.style( + "reset" + )}` + ) + + pathfs.writeFile(manifestPath, updatedManifest) + end + end + + -- Fetch README for the tool repo, if present + transactions = gh:queueTransactions({ + { + type = "Custom", + payload = { + method = "GET", + url = `https://api.github.com/repos/{repoName}/readme`, + }, + }, + }) + + local contentsResp = transactions[1] :: Result + + local readmeRes = contentsResp:andThen(function(resp: GithubContents) + -- If there was not an error, and the contents are base64 encoded, + -- we decode the contents and return the decoded buffer + if resp.encoding == "base64" then + -- NOTE: Github uses a special format for encoding the contents, where it + -- separates the entire file into multiple lines, and encodes each line in + -- base64 + + -- This should be done by the base64 library, but oh well + local content = string.gsub(resp.content, "%s+", "") + local decoded = base64.decode(buffer.fromstring(content)) + return Result.Ok(decoded) + end + + return Result.Err(`Unsupported encoding: {resp.encoding}`) + end) + + -- NOTE: Ideally this block should be a match, but I cannot make use of + -- control flow expressions from within a function + if readmeRes:isErr() then + warn(`Failed to fetch README from tool repo {repoName}:`, readmeRes:unwrapErr()) + continue + end + + -- Write the updated README to the tool's directory + local readmeContents = readmeRes:unwrap() + -- There used to be some issues with encoding if not deleted and recreated + pathfs.removeFile(readmePath) + pathfs.writeFile(readmePath, readmeContents) + + print( + `{INFO_PREFIX} Wrote README to {stdio.style("dim")}{readmePath:stripPrefix(pathfs.cwd)}{stdio.style("reset")}` + ) +end + +local timeElapsed = string.format("%.2fs", os.clock() - START_TIME) +print(`{INFO_PREFIX} Finished checking for tool updates in {stdio.style("dim")}{timeElapsed}{stdio.style("reset")}!`) From 70f77385f325e53291d6b8d365e4b704301161df Mon Sep 17 00:00:00 2001 From: ernisto Date: Fri, 25 Apr 2025 15:03:14 -0300 Subject: [PATCH 5/8] 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 From eff55f9a6d0bf8078a327a093d4865dd3f84f088 Mon Sep 17 00:00:00 2001 From: ernisto Date: Fri, 25 Apr 2025 15:07:27 -0300 Subject: [PATCH 6/8] test(zap): only test if the output has generated instead of compare with local snapshot - because will fail if zap changes the output even a little --- bins/zap/tests/run.luau | 4 +- bins/zap/tests/snapshots/client.luau | 216 ------------------------- bins/zap/tests/snapshots/server.luau | 234 --------------------------- 3 files changed, 2 insertions(+), 452 deletions(-) delete mode 100644 bins/zap/tests/snapshots/client.luau delete mode 100644 bins/zap/tests/snapshots/server.luau diff --git a/bins/zap/tests/run.luau b/bins/zap/tests/run.luau index f805d58..a4eb52d 100644 --- a/bins/zap/tests/run.luau +++ b/bins/zap/tests/run.luau @@ -8,5 +8,5 @@ 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`) +assert(fs.isFile("tests/output/client.luau"), `invalid output`) +assert(fs.isFile("tests/output/server.luau"), `invalid output`) diff --git a/bins/zap/tests/snapshots/client.luau b/bins/zap/tests/snapshots/client.luau deleted file mode 100644 index 4ac227b..0000000 --- a/bins/zap/tests/snapshots/client.luau +++ /dev/null @@ -1,216 +0,0 @@ ---!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 deleted file mode 100644 index bc77f61..0000000 --- a/bins/zap/tests/snapshots/server.luau +++ /dev/null @@ -1,234 +0,0 @@ ---!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 From e3c421556961bf261bacda850171d4a9fbc0aad1 Mon Sep 17 00:00:00 2001 From: ernisto Date: Fri, 25 Apr 2025 17:15:39 -0300 Subject: [PATCH 7/8] tests: run before change manifest, with a new version parameter --- .lune/update_tools.luau | 29 ++++++++++++++++------------- bins/zap/tests/run.luau | 3 ++- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/.lune/update_tools.luau b/.lune/update_tools.luau index cf639db..e2ae9b1 100644 --- a/.lune/update_tools.luau +++ b/.lune/update_tools.luau @@ -79,7 +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 testsPath = absPath:join("tests/run.luau") local binEntrypoint = absPath:join("init.luau") local manifestPath = absPath:join("pesde.toml") local readmePath = absPath:join("README.md") @@ -136,6 +136,21 @@ for _, binSrc in pathfs.readDir(BINS_SRC_DIR) do )}{newVersion}{stdio.style("reset")}` ) + -- 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 + continue + end + + success, result = pcall(result, newVersion) + if not success then + continue + end + else + warn(`Unit tests not found for {binSrc}, assuming that they pass`) + end + -- HACK: To prevent messing with our existing toml ordering and formatting -- we just replace the old version field string with the new version field -- string @@ -171,18 +186,6 @@ 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/run.luau b/bins/zap/tests/run.luau index a4eb52d..6fb04b0 100644 --- a/bins/zap/tests/run.luau +++ b/bins/zap/tests/run.luau @@ -2,7 +2,7 @@ local toolchainlib = require("../lune_packages/core") local process = require("@lune/process") local fs = require("@lune/fs") --- not working, needing support +return function(version) -- process.args = { "bins/zap/tests/input.zap" } local success, err = pcall(require, "bins/zap/init.luau") @@ -10,3 +10,4 @@ assert(success, `failed to execute zap: {err}`) assert(fs.isFile("tests/output/client.luau"), `invalid output`) assert(fs.isFile("tests/output/server.luau"), `invalid output`) +end From f09a1149adf460ed5764b14e537f6825c29b98cb Mon Sep 17 00:00:00 2001 From: ernisto Date: Fri, 25 Apr 2025 17:22:04 -0300 Subject: [PATCH 8/8] test: cover all tools --- bins/argon/tests/input.project.json | 6 ++++ bins/argon/tests/run.luau | 12 ++++++++ bins/asphalt/tests/asphalt.toml | 10 +++++++ bins/asphalt/tests/input/water_normal_map.png | Bin 0 -> 53364 bytes bins/asphalt/tests/run.luau | 13 ++++++++ bins/blink/tests/input.blink | 9 ++++++ bins/blink/tests/run.luau | 16 ++++++++++ bins/darklua/tests/input.luau | 8 +++++ bins/darklua/tests/run.luau | 16 ++++++++++ bins/luau-lsp/tests/input.luau | 1 + bins/luau-lsp/tests/run.luau | 15 ++++++++++ bins/rojo/tests/input.project.json | 6 ++++ bins/rojo/tests/run.luau | 12 ++++++++ bins/selene/tests/bad_input.luau | 3 ++ bins/selene/tests/good_input.luau | 3 ++ bins/selene/tests/run.luau | 28 ++++++++++++++++++ bins/stylua/tests/bad_input.luau | 1 + bins/stylua/tests/good_input.luau | 1 + bins/stylua/tests/run.luau | 22 ++++++++++++++ bins/zap/tests/run.luau | 13 ++++---- 20 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 bins/argon/tests/input.project.json create mode 100644 bins/argon/tests/run.luau create mode 100644 bins/asphalt/tests/asphalt.toml create mode 100644 bins/asphalt/tests/input/water_normal_map.png create mode 100644 bins/asphalt/tests/run.luau create mode 100644 bins/blink/tests/input.blink create mode 100644 bins/blink/tests/run.luau create mode 100644 bins/darklua/tests/input.luau create mode 100644 bins/darklua/tests/run.luau create mode 100644 bins/luau-lsp/tests/input.luau create mode 100644 bins/luau-lsp/tests/run.luau create mode 100644 bins/rojo/tests/input.project.json create mode 100644 bins/rojo/tests/run.luau create mode 100644 bins/selene/tests/bad_input.luau create mode 100644 bins/selene/tests/good_input.luau create mode 100644 bins/selene/tests/run.luau create mode 100644 bins/stylua/tests/bad_input.luau create mode 100644 bins/stylua/tests/good_input.luau create mode 100644 bins/stylua/tests/run.luau diff --git a/bins/argon/tests/input.project.json b/bins/argon/tests/input.project.json new file mode 100644 index 0000000..318bf32 --- /dev/null +++ b/bins/argon/tests/input.project.json @@ -0,0 +1,6 @@ +{ + "name": "good-input", + "tree": { + "$path": "../" + } +} \ No newline at end of file diff --git a/bins/argon/tests/run.luau b/bins/argon/tests/run.luau new file mode 100644 index 0000000..99568df --- /dev/null +++ b/bins/argon/tests/run.luau @@ -0,0 +1,12 @@ +local toolchainlib = require("../lune_packages/core") +local process = require("@lune/process") +local fs = require("@lune/fs") + +return function(version) + -- not working, needing support + -- process.args = { "sourcemap", "bins/argon/tests/input.project.json", "--output bins/argon/tests/output/sourcemap.json" } + local success, err = pcall(require, "bins/argon/init.luau") + assert(success, `failed to execute argon: {err}`) + + assert(fs.isFile("bins/argon/tests/output/sourcemap.luau"), `output not found`) +end diff --git a/bins/asphalt/tests/asphalt.toml b/bins/asphalt/tests/asphalt.toml new file mode 100644 index 0000000..eac4a5d --- /dev/null +++ b/bins/asphalt/tests/asphalt.toml @@ -0,0 +1,10 @@ +[inputs.assets] +path = "input/**" +output_path = "output" + +[creator] +type = "user" +id = 0 + +[codegen] +output_name = "init.luau" \ No newline at end of file diff --git a/bins/asphalt/tests/input/water_normal_map.png b/bins/asphalt/tests/input/water_normal_map.png new file mode 100644 index 0000000000000000000000000000000000000000..db9ab4b7b353a2f5ef70e490e3096286ba89665e GIT binary patch literal 53364 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 z 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
rf$d7ELQ|8{MV>cBqO$4g>yt`{Cg@M^MI6np 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