@isentinel/jest-roblox 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +295 -398
- package/dist/{executor-CNz6_04-.d.mts → executor-B2IDh6bH.d.mts} +176 -89
- package/dist/{game-output-C0KykXIi.mjs → game-output-CwmtpYhn.mjs} +531 -172
- package/dist/index.d.mts +301 -19
- package/dist/index.mjs +2 -2
- package/dist/sea/jest-roblox +0 -0
- package/dist/sea-entry.cjs +5157 -20039
- package/package.json +18 -14
- package/plugin/JestRobloxRunner.rbxm +0 -0
- package/plugin/out/shared/entry.luau +3 -2
- package/plugin/out/shared/promise.luau +2006 -0
- package/plugin/out/shared/runner.luau +69 -1
- package/plugin/out/shared/setup-timing.luau +89 -0
- package/plugin/src/init.server.luau +1 -1
- package/plugin/src/test-in-run-mode.server.luau +14 -4
|
@@ -4,16 +4,15 @@ import assert from "node:assert";
|
|
|
4
4
|
import * as fs$1 from "node:fs";
|
|
5
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
6
6
|
import * as path$1 from "node:path";
|
|
7
|
-
import path
|
|
7
|
+
import path from "node:path";
|
|
8
8
|
import process from "node:process";
|
|
9
9
|
import color from "tinyrainbow";
|
|
10
10
|
import { WebSocketServer } from "ws";
|
|
11
|
+
import { createFetchClient, getCacheDirectory, getCacheKey, hashBuffer, isUploaded, markUploaded, readCache, writeCache } from "@isentinel/roblox-runner";
|
|
11
12
|
import { createDefineConfig, loadConfig } from "c12";
|
|
12
|
-
import { homedir, tmpdir } from "node:os";
|
|
13
|
-
import * as crypto from "node:crypto";
|
|
14
13
|
import { randomUUID } from "node:crypto";
|
|
15
|
-
import buffer from "node:buffer";
|
|
16
14
|
import { defuFn } from "defu";
|
|
15
|
+
import { collectPaths as collectPaths$1, resolveNestedProjects } from "@isentinel/rojo-utils";
|
|
17
16
|
import { getTsconfig } from "get-tsconfig";
|
|
18
17
|
import { TraceMap, originalPositionFor, sourceContentFor } from "@jridgewell/trace-mapping";
|
|
19
18
|
import hljs from "highlight.js/lib/core";
|
|
@@ -64,6 +63,7 @@ function parseJestOutput(output) {
|
|
|
64
63
|
const parsed = JSON.parse(trimmed);
|
|
65
64
|
const coverageData = extractCoverageData(parsed);
|
|
66
65
|
const luauTiming = extractLuauTiming(parsed);
|
|
66
|
+
const setupSeconds = extractSetupSeconds(parsed);
|
|
67
67
|
const snapshotWrites = extractSnapshotWrites(parsed);
|
|
68
68
|
const unwrapped = unwrapResult(parsed);
|
|
69
69
|
if (unwrapped["kind"] === "ExecutionError") {
|
|
@@ -74,6 +74,7 @@ function parseJestOutput(output) {
|
|
|
74
74
|
coverageData,
|
|
75
75
|
luauTiming,
|
|
76
76
|
result: validateJestResult(unwrapped["results"]),
|
|
77
|
+
setupSeconds,
|
|
77
78
|
snapshotWrites
|
|
78
79
|
};
|
|
79
80
|
try {
|
|
@@ -81,6 +82,7 @@ function parseJestOutput(output) {
|
|
|
81
82
|
coverageData,
|
|
82
83
|
luauTiming,
|
|
83
84
|
result: validateJestResult(unwrapped),
|
|
85
|
+
setupSeconds,
|
|
84
86
|
snapshotWrites
|
|
85
87
|
};
|
|
86
88
|
} catch {}
|
|
@@ -193,6 +195,11 @@ function validateJestResult(value) {
|
|
|
193
195
|
if (result instanceof type.errors) throw new Error(`Invalid Jest result: ${result.summary}`);
|
|
194
196
|
return result;
|
|
195
197
|
}
|
|
198
|
+
function extractSetupSeconds(parsed) {
|
|
199
|
+
const setup = parsed["_setup"];
|
|
200
|
+
if (typeof setup !== "number") return;
|
|
201
|
+
return setup;
|
|
202
|
+
}
|
|
196
203
|
//#endregion
|
|
197
204
|
//#region src/config/schema.ts
|
|
198
205
|
const ROOT_ONLY_KEYS = new Set([
|
|
@@ -208,6 +215,7 @@ const ROOT_ONLY_KEYS = new Set([
|
|
|
208
215
|
"gameOutput",
|
|
209
216
|
"jestPath",
|
|
210
217
|
"luauRoots",
|
|
218
|
+
"parallel",
|
|
211
219
|
"placeFile",
|
|
212
220
|
"pollInterval",
|
|
213
221
|
"port",
|
|
@@ -362,6 +370,7 @@ const configSchema = type({
|
|
|
362
370
|
"maxWorkers?": type("number").or(type("string")),
|
|
363
371
|
"noStackTrace?": "boolean",
|
|
364
372
|
"outputFile?": "string",
|
|
373
|
+
"parallel?": type("'auto'").or("number.integer >= 1"),
|
|
365
374
|
"passWithNoTests?": "boolean",
|
|
366
375
|
"placeFile?": "string",
|
|
367
376
|
"pollInterval?": "number",
|
|
@@ -412,7 +421,7 @@ const defineConfig = createDefineConfig();
|
|
|
412
421
|
const defineProject = createDefineConfig();
|
|
413
422
|
//#endregion
|
|
414
423
|
//#region src/test-runner.bundled.luau
|
|
415
|
-
var test_runner_bundled_default = "type PatchState__DARKLUA_TYPE_a = {\n robloxSharedExports: any,\n originalGetDataModelService: any,\n Runtime: any,\n originalRequireInternalModule: any,\n}\n\ntype Config__DARKLUA_TYPE_b = {\n jestPath: string?,\n projects: { string }?,\n setupFiles: { string }?,\n setupFilesAfterEnv: { string }?,\n _coverage: boolean?,\n _timing: boolean?,\n}\n\ntype CapturedMessage__DARKLUA_TYPE_c = { message: string, messageType: number, timestamp: number }\nlocal __JEST_MODULES={cache={}::any}do do local function __modImpl()--!strict\n\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal module = {}\n\nfunction module.findInstance(path: string): Instance\n local parts = string.split(path, \"/\")\n\n local success, current = pcall(function()\n return game:FindService(parts[1])\n end)\n assert(success, `Failed to find service {parts[1]}: {current}`)\n\n for i = 2, #parts do\n assert(current, `Failed to find '{parts[i - 1]}' in path {path}`)\n current = current:FindFirstChild(parts[i])\n end\n\n assert(current, `Failed to find instance at path {path}`)\n\n return current\nend\n\nfunction module.getJest(config: { jestPath: string? }): ModuleScript\n local jestPath = config.jestPath\n if jestPath then\n local instance = module.findInstance(jestPath)\n assert(instance, `Failed to find Jest instance at path {jestPath}`)\n assert(instance:IsA(\"ModuleScript\"), `Instance at path {jestPath} is not a ModuleScript`)\n return instance :: ModuleScript\n end\n\n local jestInstance = ReplicatedStorage:FindFirstChild(\"Jest\", true)\n assert(jestInstance, \"Failed to find Jest instance in ReplicatedStorage\")\n assert(jestInstance:IsA(\"ModuleScript\"), \"Jest instance in ReplicatedStorage is not a ModuleScript\")\n return jestInstance\nend\n\nfunction module.findRobloxShared(jestModule: ModuleScript): Instance?\n local parent = jestModule.Parent\n if parent then\n local found = parent:FindFirstChild(\"RobloxShared\") or parent:FindFirstChild(\"jest-roblox-shared\")\n if found then\n return found\n end\n\n if parent.Parent then\n found = parent.Parent:FindFirstChild(\"RobloxShared\") or parent.Parent:FindFirstChild(\"jest-roblox-shared\")\n if found then\n return found\n end\n end\n end\n\n return game:FindFirstChild(\"RobloxShared\", true)\nend\n\nfunction module.findSiblingPackage(jestModule: ModuleScript, ...: string): Instance?\n local parent = jestModule.Parent\n if parent then\n for _, name in { ... } do\n local found = parent:FindFirstChild(name)\n if found then\n return found\n end\n end\n\n if parent.Parent then\n for _, name in { ... } do\n local found = parent.Parent:FindFirstChild(name)\n if found then\n return found\n end\n end\n end\n end\n\n for _, name in { ... } do\n local found = game:FindFirstChild(name, true)\n if found then\n return found\n end\n end\n\n return nil\nend\n\nreturn module\nend function __JEST_MODULES.a():typeof(__modImpl())local v=__JEST_MODULES.cache.a if not v then v={c=__modImpl()}__JEST_MODULES.cache.a=v end return v.c end end do local function __modImpl()--!strict\n\nlocal function getInstancePath(instance: Instance): string\n local parts = {} :: { string }\n local current: Instance? = instance\n while current and current ~= game do\n table.insert(parts, 1, current.Name)\n current = current.Parent\n end\n\n return table.concat(parts, \"/\")\nend\n\nlocal CoreScriptSyncService = {}\n\nfunction CoreScriptSyncService:GetScriptFilePath(instance: Instance): string\n return getInstancePath(instance)\nend\n\nreturn CoreScriptSyncService\nend function __JEST_MODULES.b():typeof(__modImpl())local v=__JEST_MODULES.cache.b if not v then v={c=__modImpl()}__JEST_MODULES.cache.b=v end return v.c end end do local function __modImpl()--!strict\n\nlocal function normalizeSnapPath(path: string): string\n return (string.gsub(path, \"%.snap%.lua$\", \".snap.luau\"))\nend\n\nlocal function create(snapshotWrites: { [string]: string })\n local FileSystemService = {}\n\n function FileSystemService:WriteFile(path: string, contents: string)\n snapshotWrites[normalizeSnapPath(path)] = contents\n end\n\n function FileSystemService:CreateDirectories(_path: string) end\n\n function FileSystemService:Exists(path: string): boolean\n return snapshotWrites[normalizeSnapPath(path)] ~= nil\n end\n\n function FileSystemService:Remove(path: string)\n snapshotWrites[normalizeSnapPath(path)] = nil\n end\n\n function FileSystemService:IsRegularFile(path: string): boolean\n return snapshotWrites[normalizeSnapPath(path)] ~= nil\n end\n\n return FileSystemService\nend\n\nreturn create\nend function __JEST_MODULES.c():typeof(__modImpl())local v=__JEST_MODULES.cache.c if not v then v={c=__modImpl()}__JEST_MODULES.cache.c=v end return v.c end end do local function __modImpl()--!strict\n\nlocal CoreScriptSyncService = __JEST_MODULES.b()\nlocal InstanceResolver = __JEST_MODULES.a()\n\nlocal module = {}\n\nfunction module.createMockGetDataModelService(snapshotWrites: { [string]: string })\n local FileSystemService = __JEST_MODULES.c()(snapshotWrites)\n\n return function(service: string): any\n if service == \"FileSystemService\" then\n return FileSystemService\n elseif service == \"CoreScriptSyncService\" then\n return CoreScriptSyncService\n end\n\n local success, result = pcall(function()\n local service_ = game:GetService(service)\n local _ = service_.Name\n return service_\n end)\n\n return if success then result else nil\n end\nend\n\n\n\n\n\n\n\n\nfunction module.patch(jestModule: ModuleScript, snapshotWrites: { [string]: string }): PatchState__DARKLUA_TYPE_a?\n local mockGetDataModelService = module.createMockGetDataModelService(snapshotWrites)\n\n local robloxSharedInstance = InstanceResolver.findRobloxShared(jestModule)\n if not robloxSharedInstance then\n warn(\"Could not find RobloxShared; snapshot support unavailable\")\n return nil\n end\n\n local robloxSharedExports = (require :: any)(robloxSharedInstance)\n local originalGetDataModelService = robloxSharedExports.getDataModelService\n robloxSharedExports.getDataModelService = mockGetDataModelService\n\n local getDataModelServiceChild = robloxSharedInstance:FindFirstChild(\"getDataModelService\")\n\n local jestRuntimeModule = InstanceResolver.findSiblingPackage(jestModule, \"JestRuntime\", \"jest-runtime\")\n if not jestRuntimeModule then\n warn(\"Could not find JestRuntime; snapshot interception unavailable\")\n return {\n robloxSharedExports = robloxSharedExports,\n originalGetDataModelService = originalGetDataModelService,\n Runtime = nil,\n originalRequireInternalModule = nil,\n }\n end\n\n local Runtime = (require :: any)(jestRuntimeModule)\n local originalRequireInternalModule = Runtime.requireInternalModule\n\n Runtime.requireInternalModule = function(self: any, from: any, to: any, ...): any\n local target = if to ~= nil then to else from\n if getDataModelServiceChild and typeof(target) == \"Instance\" and target == getDataModelServiceChild then\n return mockGetDataModelService\n end\n\n return originalRequireInternalModule(self, from, to, ...)\n end\n\n return {\n robloxSharedExports = robloxSharedExports,\n originalGetDataModelService = originalGetDataModelService,\n Runtime = Runtime,\n originalRequireInternalModule = originalRequireInternalModule,\n }\nend\n\nfunction module.unpatch(state: PatchState__DARKLUA_TYPE_a?)\n if not state then\n return\n end\n\n if state.robloxSharedExports and state.originalGetDataModelService then\n state.robloxSharedExports.getDataModelService = state.originalGetDataModelService\n end\n\n if state.Runtime and state.originalRequireInternalModule then\n state.Runtime.requireInternalModule = state.originalRequireInternalModule\n end\nend\n\nreturn module\nend function __JEST_MODULES.d():typeof(__modImpl())local v=__JEST_MODULES.cache.d if not v then v={c=__modImpl()}__JEST_MODULES.cache.d=v end return v.c end end do local function __modImpl()--!strict\n\nlocal HttpService = game:GetService(\"HttpService\")\nlocal LogService = game:GetService(\"LogService\")\n\nlocal InstanceResolver = __JEST_MODULES.a()\nlocal SnapshotPatch = __JEST_MODULES.d()\n\n\n\n\n\n\n\n\n\n\nlocal function fail(err: string)\n return {\n success = false,\n err = err,\n }\nend\n\n\n\nlocal function interceptWriteable(writeable: any, buffer: { CapturedMessage__DARKLUA_TYPE_c }, messageType: number)\n local original = writeable._writeFn\n if typeof(original) ~= \"function\" then\n return function() end\n end\n\n writeable._writeFn = function(data: string)\n table.insert(buffer, {\n message = data,\n messageType = messageType,\n timestamp = os.clock(),\n })\n original(data)\n end\n\n return function()\n writeable._writeFn = original\n end\nend\n\nlocal module = {}\n\nfunction module.run(callingScript: LuaSourceContainer, config: Config__DARKLUA_TYPE_b): (string, string)\n local t0 = os.clock()\n local timingEnabled = config._timing\n local coverageEnabled = config._coverage\n\n local t_findJest0 = os.clock()\n local findSuccess, findValue = pcall(InstanceResolver.getJest, config)\n local t_findJest = os.clock()\n\n if not findSuccess then\n local logSuccess, logHistory = pcall(function()\n return HttpService:JSONEncode(LogService:GetLogHistory())\n end)\n\n return HttpService:JSONEncode(fail(findValue :: any)), if logSuccess then logHistory else \"[]\"\n end\n\n LogService:ClearOutput()\n\n local snapshotWrites: { [string]: string } = {}\n\n local t_patchSnapshot0 = os.clock()\n local patchState = SnapshotPatch.patch(findValue, snapshotWrites)\n local t_patchSnapshot = os.clock()\n\n local t_requireJest0 = os.clock()\n local Jest = (require :: any)(findValue)\n local t_requireJest = os.clock()\n\n -- Intercept Jest's stdout/stderr to capture output synchronously.\n -- Jest writes via process.stdout/stderr (Writeable objects whose _writeFn\n -- defaults to print). Wrapping _writeFn captures messages like\n -- \"No tests found\" that are printed just before exit(1) throws.\n local capturedMessages: { CapturedMessage__DARKLUA_TYPE_c } = {}\n local restoreStdout: (() -> ())?\n local restoreStderr: (() -> ())?\n\n local interceptOk = pcall(function()\n local nodeModules = findValue.Parent.Parent.Parent :: any\n local RobloxShared = (require :: any)(nodeModules[\"@rbxts-js\"].RobloxShared)\n local process = RobloxShared.nodeUtils.process\n\n restoreStdout = interceptWriteable(process.stdout, capturedMessages, 0)\n restoreStderr = interceptWriteable(process.stderr, capturedMessages, 1)\n end)\n\n if not interceptOk then\n restoreStdout = nil\n restoreStderr = nil\n end\n\n local function runTests()\n local t_resolveProjects0 = os.clock()\n local projects = {}\n\n assert(\n config.projects and #config.projects > 0,\n \"No projects configured. Set 'projects' in jest.config.ts or pass --projects.\"\n )\n\n for _, projectPath in config.projects do\n table.insert(projects, InstanceResolver.findInstance(projectPath))\n end\n\n config.projects = {}\n local t_resolveProjects = os.clock()\n\n local t_resolveSetupFiles0 = os.clock()\n if config.setupFiles and #config.setupFiles > 0 then\n local resolved = {}\n\n for _, setupPath in config.setupFiles do\n table.insert(resolved, InstanceResolver.findInstance(setupPath))\n end\n\n config.setupFiles = resolved :: any\n end\n if config.setupFilesAfterEnv and #config.setupFilesAfterEnv > 0 then\n local resolved = {}\n\n for _, setupPath in config.setupFilesAfterEnv do\n table.insert(resolved, InstanceResolver.findInstance(setupPath))\n end\n\n config.setupFilesAfterEnv = resolved :: any\n end\n local t_resolveSetupFiles = os.clock()\n\n -- Strip private keys before Jest.runCLI (safe: single-task execution per VM)\n config._timing = nil :: any\n config._coverage = nil :: any\n\n if coverageEnabled then\n _G.__jest_roblox_cov = {}\n end\n\n local t_jestRunCLI0 = os.clock()\n local jestResult = Jest.runCLI(callingScript, config, projects):expect()\n local t_jestRunCLI = os.clock()\n\n local result: { [string]: any } = {\n success = true,\n value = jestResult,\n }\n\n if timingEnabled then\n result._timing = {\n findJest = t_findJest - t_findJest0,\n patchSnapshot = t_patchSnapshot - t_patchSnapshot0,\n requireJest = t_requireJest - t_requireJest0,\n resolveProjects = t_resolveProjects - t_resolveProjects0,\n resolveSetupFiles = t_resolveSetupFiles - t_resolveSetupFiles0,\n jestRunCLI = t_jestRunCLI - t_jestRunCLI0,\n total = os.clock() - t0,\n }\n end\n\n if next(snapshotWrites) then\n result._snapshotWrites = snapshotWrites\n end\n\n if coverageEnabled then\n result._coverage = _G.__jest_roblox_cov\n end\n\n return result\n end\n\n local jestDone = false\n local runSuccess = false\n local runValue: any = nil\n\n task.spawn(function()\n local ok, val = pcall(runTests)\n jestDone = true\n runSuccess = ok\n runValue = val\n end)\n\n local infiniteYieldMessage: string? = nil\n local watchdogConnection = LogService.MessageOut:Connect(function(message: string, messageType: Enum.MessageType)\n if\n messageType == Enum.MessageType.MessageWarning\n and string.find(message, \"Infinite yield possible\")\n and not infiniteYieldMessage\n then\n infiniteYieldMessage = message\n end\n end)\n\n while not jestDone and not infiniteYieldMessage do\n task.wait(0.1)\n end\n\n watchdogConnection:Disconnect()\n\n if restoreStdout then\n restoreStdout()\n end\n\n if restoreStderr then\n restoreStderr()\n end\n\n if not jestDone and infiniteYieldMessage then\n runSuccess = false\n runValue = \"Infinite yield detected, aborting tests: \" .. infiniteYieldMessage\n end\n\n SnapshotPatch.unpatch(patchState)\n\n local jestResult\n if not runSuccess then\n jestResult = HttpService:JSONEncode(fail(runValue :: any))\n else\n jestResult = HttpService:JSONEncode(runValue)\n end\n\n local logSuccess, logHistory = pcall(function()\n return HttpService:JSONEncode(capturedMessages)\n end)\n\n return jestResult, if logSuccess then logHistory else \"[]\"\nend\n\nreturn module\nend function __JEST_MODULES.e():typeof(__modImpl())local v=__JEST_MODULES.cache.e if not v then v={c=__modImpl()}__JEST_MODULES.cache.e=v end return v.c end end end--!strict\n\nlocal HttpService = game:GetService(\"HttpService\")\n\nlocal Runner = __JEST_MODULES.e()\n\nlocal config = HttpService:JSONDecode([=[__CONFIG_JSON__]=])\n\nreturn Runner.run(script, config)\n";
|
|
424
|
+
var test_runner_bundled_default = "type PatchState__DARKLUA_TYPE_a = {\n Runtime: any,\n originalRequireModule: any,\n accumulatedSeconds: { value: number },\n}\n\ntype PatchState__DARKLUA_TYPE_b = {\n robloxSharedExports: any,\n originalGetDataModelService: any,\n Runtime: any,\n originalRequireInternalModule: any,\n}\n\ntype Config__DARKLUA_TYPE_c = {\n jestPath: string?,\n projects: { string }?,\n setupFiles: { string }?,\n setupFilesAfterEnv: { string }?,\n _coverage: boolean?,\n _timing: boolean?,\n}\n\ntype CapturedMessage__DARKLUA_TYPE_d = { message: string, messageType: number, timestamp: number }\n\ntype ProjectEntry__DARKLUA_TYPE_e = {\n jestOutput: string,\n gameOutput: string,\n elapsedMs: number,\n}\nlocal __JEST_MODULES={cache={}::any}do do local function __modImpl()--!strict\n\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal module = {}\n\nfunction module.findInstance(path: string): Instance\n local parts = string.split(path, \"/\")\n\n local success, current = pcall(function()\n return game:FindService(parts[1])\n end)\n assert(success, `Failed to find service {parts[1]}: {current}`)\n\n for i = 2, #parts do\n assert(current, `Failed to find '{parts[i - 1]}' in path {path}`)\n current = current:FindFirstChild(parts[i])\n end\n\n assert(current, `Failed to find instance at path {path}`)\n\n return current\nend\n\nfunction module.getJest(config: { jestPath: string? }): ModuleScript\n local jestPath = config.jestPath\n if jestPath then\n local instance = module.findInstance(jestPath)\n assert(instance, `Failed to find Jest instance at path {jestPath}`)\n assert(instance:IsA(\"ModuleScript\"), `Instance at path {jestPath} is not a ModuleScript`)\n return instance :: ModuleScript\n end\n\n local jestInstance = ReplicatedStorage:FindFirstChild(\"Jest\", true)\n assert(jestInstance, \"Failed to find Jest instance in ReplicatedStorage\")\n assert(jestInstance:IsA(\"ModuleScript\"), \"Jest instance in ReplicatedStorage is not a ModuleScript\")\n return jestInstance\nend\n\nfunction module.findRobloxShared(jestModule: ModuleScript): Instance?\n local parent = jestModule.Parent\n if parent then\n local found = parent:FindFirstChild(\"RobloxShared\") or parent:FindFirstChild(\"jest-roblox-shared\")\n if found then\n return found\n end\n\n if parent.Parent then\n found = parent.Parent:FindFirstChild(\"RobloxShared\") or parent.Parent:FindFirstChild(\"jest-roblox-shared\")\n if found then\n return found\n end\n end\n end\n\n return game:FindFirstChild(\"RobloxShared\", true)\nend\n\nfunction module.findSiblingPackage(jestModule: ModuleScript, ...: string): Instance?\n local parent = jestModule.Parent\n if parent then\n for _, name in { ... } do\n local found = parent:FindFirstChild(name)\n if found then\n return found\n end\n end\n\n if parent.Parent then\n for _, name in { ... } do\n local found = parent.Parent:FindFirstChild(name)\n if found then\n return found\n end\n end\n end\n end\n\n for _, name in { ... } do\n local found = game:FindFirstChild(name, true)\n if found then\n return found\n end\n end\n\n return nil\nend\n\nreturn module\nend function __JEST_MODULES.a():typeof(__modImpl())local v=__JEST_MODULES.cache.a if not v then v={c=__modImpl()}__JEST_MODULES.cache.a=v end return v.c end end do local function __modImpl()--!strict\n\nlocal InstanceResolver = __JEST_MODULES.a()\n\nlocal module = {}\n\n\n\n\n\n\n\nfunction module.patch(\n jestModule: ModuleScript,\n setupFiles: { Instance }?,\n setupFilesAfterEnv: { Instance }?\n): PatchState__DARKLUA_TYPE_a?\n local setupSet: { [Instance]: boolean } = {}\n\n if setupFiles then\n for _, inst in setupFiles do\n setupSet[inst] = true\n end\n end\n\n if setupFilesAfterEnv then\n for _, inst in setupFilesAfterEnv do\n setupSet[inst] = true\n end\n end\n\n if not next(setupSet) then\n return nil\n end\n\n local jestRuntimeModule = InstanceResolver.findSiblingPackage(jestModule, \"JestRuntime\", \"jest-runtime\")\n if not jestRuntimeModule then\n warn(\"Could not find JestRuntime; setup timing unavailable\")\n return nil\n end\n\n local Runtime = (require :: any)(jestRuntimeModule)\n local originalRequireModule = Runtime.requireModule\n local accumulated = { value = 0 }\n local insideSetupRequire = false\n\n Runtime.requireModule = function(self: any, moduleName: any, ...): any\n if not insideSetupRequire and typeof(moduleName) == \"Instance\" and setupSet[moduleName] then\n insideSetupRequire = true\n local t0 = os.clock()\n local results = table.pack(pcall(originalRequireModule, self, moduleName, ...))\n accumulated.value += os.clock() - t0\n insideSetupRequire = false\n\n if not results[1] then\n error(results[2], 0)\n end\n\n return table.unpack(results, 2, results.n)\n end\n\n return originalRequireModule(self, moduleName, ...)\n end\n\n return {\n Runtime = Runtime,\n originalRequireModule = originalRequireModule,\n accumulatedSeconds = accumulated,\n }\nend\n\nfunction module.unpatch(state: PatchState__DARKLUA_TYPE_a?)\n if not state then\n return\n end\n\n if state.Runtime and state.originalRequireModule then\n state.Runtime.requireModule = state.originalRequireModule\n end\nend\n\nfunction module.getSeconds(state: PatchState__DARKLUA_TYPE_a?): number\n if not state then\n return 0\n end\n\n return state.accumulatedSeconds.value\nend\n\nreturn module\nend function __JEST_MODULES.b():typeof(__modImpl())local v=__JEST_MODULES.cache.b if not v then v={c=__modImpl()}__JEST_MODULES.cache.b=v end return v.c end end do local function __modImpl()--!strict\n\nlocal function getInstancePath(instance: Instance): string\n local parts = {} :: { string }\n local current: Instance? = instance\n while current and current ~= game do\n table.insert(parts, 1, current.Name)\n current = current.Parent\n end\n\n return table.concat(parts, \"/\")\nend\n\nlocal CoreScriptSyncService = {}\n\nfunction CoreScriptSyncService:GetScriptFilePath(instance: Instance): string\n return getInstancePath(instance)\nend\n\nreturn CoreScriptSyncService\nend function __JEST_MODULES.c():typeof(__modImpl())local v=__JEST_MODULES.cache.c if not v then v={c=__modImpl()}__JEST_MODULES.cache.c=v end return v.c end end do local function __modImpl()--!strict\n\nlocal function normalizeSnapPath(path: string): string\n return (string.gsub(path, \"%.snap%.lua$\", \".snap.luau\"))\nend\n\nlocal function create(snapshotWrites: { [string]: string })\n local FileSystemService = {}\n\n function FileSystemService:WriteFile(path: string, contents: string)\n snapshotWrites[normalizeSnapPath(path)] = contents\n end\n\n function FileSystemService:CreateDirectories(_path: string) end\n\n function FileSystemService:Exists(path: string): boolean\n return snapshotWrites[normalizeSnapPath(path)] ~= nil\n end\n\n function FileSystemService:Remove(path: string)\n snapshotWrites[normalizeSnapPath(path)] = nil\n end\n\n function FileSystemService:IsRegularFile(path: string): boolean\n return snapshotWrites[normalizeSnapPath(path)] ~= nil\n end\n\n return FileSystemService\nend\n\nreturn create\nend function __JEST_MODULES.d():typeof(__modImpl())local v=__JEST_MODULES.cache.d if not v then v={c=__modImpl()}__JEST_MODULES.cache.d=v end return v.c end end do local function __modImpl()--!strict\n\nlocal CoreScriptSyncService = __JEST_MODULES.c()\nlocal InstanceResolver = __JEST_MODULES.a()\n\nlocal module = {}\n\nfunction module.createMockGetDataModelService(snapshotWrites: { [string]: string })\n local FileSystemService = __JEST_MODULES.d()(snapshotWrites)\n\n return function(service: string): any\n if service == \"FileSystemService\" then\n return FileSystemService\n elseif service == \"CoreScriptSyncService\" then\n return CoreScriptSyncService\n end\n\n local success, result = pcall(function()\n local service_ = game:GetService(service)\n local _ = service_.Name\n return service_\n end)\n\n return if success then result else nil\n end\nend\n\n\n\n\n\n\n\n\nfunction module.patch(jestModule: ModuleScript, snapshotWrites: { [string]: string }): PatchState__DARKLUA_TYPE_b?\n local mockGetDataModelService = module.createMockGetDataModelService(snapshotWrites)\n\n local robloxSharedInstance = InstanceResolver.findRobloxShared(jestModule)\n if not robloxSharedInstance then\n warn(\"Could not find RobloxShared; snapshot support unavailable\")\n return nil\n end\n\n local robloxSharedExports = (require :: any)(robloxSharedInstance)\n local originalGetDataModelService = robloxSharedExports.getDataModelService\n robloxSharedExports.getDataModelService = mockGetDataModelService\n\n local getDataModelServiceChild = robloxSharedInstance:FindFirstChild(\"getDataModelService\")\n\n local jestRuntimeModule = InstanceResolver.findSiblingPackage(jestModule, \"JestRuntime\", \"jest-runtime\")\n if not jestRuntimeModule then\n warn(\"Could not find JestRuntime; snapshot interception unavailable\")\n return {\n robloxSharedExports = robloxSharedExports,\n originalGetDataModelService = originalGetDataModelService,\n Runtime = nil,\n originalRequireInternalModule = nil,\n }\n end\n\n local Runtime = (require :: any)(jestRuntimeModule)\n local originalRequireInternalModule = Runtime.requireInternalModule\n\n Runtime.requireInternalModule = function(self: any, from: any, to: any, ...): any\n local target = if to ~= nil then to else from\n if getDataModelServiceChild and typeof(target) == \"Instance\" and target == getDataModelServiceChild then\n return mockGetDataModelService\n end\n\n return originalRequireInternalModule(self, from, to, ...)\n end\n\n return {\n robloxSharedExports = robloxSharedExports,\n originalGetDataModelService = originalGetDataModelService,\n Runtime = Runtime,\n originalRequireInternalModule = originalRequireInternalModule,\n }\nend\n\nfunction module.unpatch(state: PatchState__DARKLUA_TYPE_b?)\n if not state then\n return\n end\n\n if state.robloxSharedExports and state.originalGetDataModelService then\n state.robloxSharedExports.getDataModelService = state.originalGetDataModelService\n end\n\n if state.Runtime and state.originalRequireInternalModule then\n state.Runtime.requireInternalModule = state.originalRequireInternalModule\n end\nend\n\nreturn module\nend function __JEST_MODULES.e():typeof(__modImpl())local v=__JEST_MODULES.cache.e if not v then v={c=__modImpl()}__JEST_MODULES.cache.e=v end return v.c end end do local function __modImpl()--!strict\n\nlocal HttpService = game:GetService(\"HttpService\")\nlocal LogService = game:GetService(\"LogService\")\n\nlocal InstanceResolver = __JEST_MODULES.a()\nlocal SetupTiming = __JEST_MODULES.b()\nlocal SnapshotPatch = __JEST_MODULES.e()\n\n\n\n\n\n\n\n\n\n\nlocal function fail(err: string)\n return {\n success = false,\n err = err,\n }\nend\n\n\n\nlocal function interceptWriteable(writeable: any, buffer: { CapturedMessage__DARKLUA_TYPE_d }, messageType: number)\n local original = writeable._writeFn\n if typeof(original) ~= \"function\" then\n return function() end\n end\n\n writeable._writeFn = function(data: string)\n table.insert(buffer, {\n message = data,\n messageType = messageType,\n timestamp = os.clock(),\n })\n original(data)\n end\n\n return function()\n writeable._writeFn = original\n end\nend\n\nlocal module = {}\n\nfunction module.run(callingScript: LuaSourceContainer, config: Config__DARKLUA_TYPE_c): (string, string)\n local t0 = os.clock()\n local timingEnabled = config._timing\n local coverageEnabled = config._coverage\n\n local t_findJest0 = os.clock()\n local findSuccess, findValue = pcall(InstanceResolver.getJest, config)\n local t_findJest = os.clock()\n\n if not findSuccess then\n local logSuccess, logHistory = pcall(function()\n return HttpService:JSONEncode(LogService:GetLogHistory())\n end)\n\n return HttpService:JSONEncode(fail(findValue :: any)), if logSuccess then logHistory else \"[]\"\n end\n\n LogService:ClearOutput()\n\n local snapshotWrites: { [string]: string } = {}\n\n local t_patchSnapshot0 = os.clock()\n local patchState = SnapshotPatch.patch(findValue, snapshotWrites)\n local t_patchSnapshot = os.clock()\n\n local t_requireJest0 = os.clock()\n local Jest = (require :: any)(findValue)\n local t_requireJest = os.clock()\n\n -- Intercept Jest's stdout/stderr to capture output synchronously.\n -- Jest writes via process.stdout/stderr (Writeable objects whose _writeFn\n -- defaults to print). Wrapping _writeFn captures messages like\n -- \"No tests found\" that are printed just before exit(1) throws.\n local capturedMessages: { CapturedMessage__DARKLUA_TYPE_d } = {}\n local restoreStdout: (() -> ())?\n local restoreStderr: (() -> ())?\n\n local interceptOk = pcall(function()\n local nodeModules = findValue.Parent.Parent.Parent :: any\n local RobloxShared = (require :: any)(nodeModules[\"@rbxts-js\"].RobloxShared)\n local process = RobloxShared.nodeUtils.process\n\n restoreStdout = interceptWriteable(process.stdout, capturedMessages, 0)\n restoreStderr = interceptWriteable(process.stderr, capturedMessages, 1)\n end)\n\n if not interceptOk then\n restoreStdout = nil\n restoreStderr = nil\n end\n\n local function runTests()\n local t_resolveProjects0 = os.clock()\n local projects = {}\n\n assert(\n config.projects and #config.projects > 0,\n \"No projects configured. Set 'projects' in jest.config.ts or pass --projects.\"\n )\n\n for _, projectPath in config.projects do\n table.insert(projects, InstanceResolver.findInstance(projectPath))\n end\n\n config.projects = {}\n local t_resolveProjects = os.clock()\n\n local t_resolveSetupFiles0 = os.clock()\n if config.setupFiles and #config.setupFiles > 0 then\n local resolved = {}\n\n for _, setupPath in config.setupFiles do\n table.insert(resolved, InstanceResolver.findInstance(setupPath))\n end\n\n config.setupFiles = resolved :: any\n end\n if config.setupFilesAfterEnv and #config.setupFilesAfterEnv > 0 then\n local resolved = {}\n\n for _, setupPath in config.setupFilesAfterEnv do\n table.insert(resolved, InstanceResolver.findInstance(setupPath))\n end\n\n config.setupFilesAfterEnv = resolved :: any\n end\n local t_resolveSetupFiles = os.clock()\n\n local setupTimingState = SetupTiming.patch(\n findValue,\n config.setupFiles :: any,\n config.setupFilesAfterEnv :: any\n )\n\n -- Strip private keys before Jest.runCLI (safe: single-task execution per VM)\n config._timing = nil :: any\n config._coverage = nil :: any\n\n if coverageEnabled then\n _G.__jest_roblox_cov = {}\n end\n\n local t_jestRunCLI0 = os.clock()\n local runCLIOk, runCLIValue = pcall(function()\n return Jest.runCLI(callingScript, config, projects):expect()\n end)\n local t_jestRunCLI = os.clock()\n\n local setupSeconds = SetupTiming.getSeconds(setupTimingState)\n SetupTiming.unpatch(setupTimingState)\n\n if not runCLIOk then\n error(runCLIValue, 0)\n end\n\n local jestResult = runCLIValue\n\n local result: { [string]: any } = {\n success = true,\n value = jestResult,\n }\n\n if setupSeconds > 0 then\n result._setup = setupSeconds\n end\n\n if timingEnabled then\n result._timing = {\n findJest = t_findJest - t_findJest0,\n patchSnapshot = t_patchSnapshot - t_patchSnapshot0,\n requireJest = t_requireJest - t_requireJest0,\n resolveProjects = t_resolveProjects - t_resolveProjects0,\n resolveSetupFiles = t_resolveSetupFiles - t_resolveSetupFiles0,\n jestRunCLI = t_jestRunCLI - t_jestRunCLI0,\n total = os.clock() - t0,\n }\n end\n\n if next(snapshotWrites) then\n result._snapshotWrites = snapshotWrites\n end\n\n if coverageEnabled then\n result._coverage = _G.__jest_roblox_cov\n end\n\n return result\n end\n\n local jestDone = false\n local runSuccess = false\n local runValue: any = nil\n\n task.spawn(function()\n local ok, val = pcall(runTests)\n jestDone = true\n runSuccess = ok\n runValue = val\n end)\n\n local infiniteYieldMessage: string? = nil\n local watchdogConnection = LogService.MessageOut:Connect(function(message: string, messageType: Enum.MessageType)\n if\n messageType == Enum.MessageType.MessageWarning\n and string.find(message, \"Infinite yield possible\")\n and not infiniteYieldMessage\n then\n infiniteYieldMessage = message\n end\n end)\n\n while not jestDone and not infiniteYieldMessage do\n task.wait(0.1)\n end\n\n watchdogConnection:Disconnect()\n\n if restoreStdout then\n restoreStdout()\n end\n\n if restoreStderr then\n restoreStderr()\n end\n\n if not jestDone and infiniteYieldMessage then\n runSuccess = false\n runValue = \"Infinite yield detected, aborting tests: \" .. infiniteYieldMessage\n end\n\n SnapshotPatch.unpatch(patchState)\n\n local jestResult\n if not runSuccess then\n jestResult = HttpService:JSONEncode(fail(runValue :: any))\n else\n jestResult = HttpService:JSONEncode(runValue)\n end\n\n local logSuccess, logHistory = pcall(function()\n return HttpService:JSONEncode(capturedMessages)\n end)\n\n return jestResult, if logSuccess then logHistory else \"[]\"\nend\n\n\n\n\n\n\n\nlocal function encodeExecutionError(err: any): string\n return HttpService:JSONEncode({\n success = true,\n value = {\n kind = \"ExecutionError\",\n error = tostring(err),\n },\n })\nend\n\n-- TODO(runner-tests): dogfood harness for Runner.runProjects\nfunction module.runProjects(\n callingScript: LuaSourceContainer,\n configs: { Config__DARKLUA_TYPE_c }\n): { ProjectEntry__DARKLUA_TYPE_e }\n local entries: { ProjectEntry__DARKLUA_TYPE_e } = {}\n\n for index, cfg in configs do\n local start = os.clock()\n local ok, jestOutput, gameOutput = pcall(module.run, callingScript, cfg)\n local elapsedMs = math.floor((os.clock() - start) * 1000)\n\n if ok then\n entries[index] = {\n jestOutput = jestOutput :: string,\n gameOutput = gameOutput :: string,\n elapsedMs = elapsedMs,\n }\n else\n entries[index] = {\n jestOutput = encodeExecutionError(jestOutput),\n gameOutput = \"[]\",\n elapsedMs = elapsedMs,\n }\n end\n end\n\n return entries\nend\n\nreturn module\nend function __JEST_MODULES.f():typeof(__modImpl())local v=__JEST_MODULES.cache.f if not v then v={c=__modImpl()}__JEST_MODULES.cache.f=v end return v.c end end end--!strict\n\nlocal HttpService = game:GetService(\"HttpService\")\n\nlocal Runner = __JEST_MODULES.f()\n\nlocal payload = HttpService:JSONDecode([=[__CONFIG_JSON__]=])\nlocal entries = Runner.runProjects(script, payload.configs)\n\nreturn HttpService:JSONEncode({ entries = entries }), \"[]\"\n";
|
|
416
425
|
//#endregion
|
|
417
426
|
//#region src/test-script.ts
|
|
418
427
|
function buildJestArgv(options) {
|
|
@@ -428,80 +437,12 @@ function buildJestArgv(options) {
|
|
|
428
437
|
};
|
|
429
438
|
}
|
|
430
439
|
function generateTestScript(options) {
|
|
431
|
-
const
|
|
432
|
-
return test_runner_bundled_default.replace("__CONFIG_JSON__", () => JSON.stringify(
|
|
433
|
-
}
|
|
434
|
-
//#endregion
|
|
435
|
-
//#region src/utils/cache.ts
|
|
436
|
-
const CACHE_DIR_NAME = "jest-roblox";
|
|
437
|
-
function getCacheDirectory() {
|
|
438
|
-
const xdgCacheHome = process.env["XDG_CACHE_HOME"];
|
|
439
|
-
if (xdgCacheHome !== void 0 && xdgCacheHome !== "") return path$1.join(xdgCacheHome, CACHE_DIR_NAME);
|
|
440
|
-
if (process.platform === "win32") {
|
|
441
|
-
const localAppData = process.env["LOCALAPPDATA"];
|
|
442
|
-
if (localAppData !== void 0 && localAppData !== "") return path$1.join(localAppData, CACHE_DIR_NAME);
|
|
443
|
-
return path$1.join(tmpdir(), CACHE_DIR_NAME);
|
|
444
|
-
}
|
|
445
|
-
return path$1.join(homedir(), ".cache", CACHE_DIR_NAME);
|
|
446
|
-
}
|
|
447
|
-
function getCacheKey(universeId, placeId) {
|
|
448
|
-
return `${universeId}:${placeId}`;
|
|
449
|
-
}
|
|
450
|
-
function isUploaded(cache, key, fileHash) {
|
|
451
|
-
return cache[key]?.fileHash === fileHash;
|
|
452
|
-
}
|
|
453
|
-
function markUploaded(cache, key, fileHash) {
|
|
454
|
-
cache[key] = {
|
|
455
|
-
fileHash,
|
|
456
|
-
uploadedAt: Date.now()
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
function readCache(cacheFilePath) {
|
|
460
|
-
try {
|
|
461
|
-
const data = fs$1.readFileSync(cacheFilePath, "utf-8");
|
|
462
|
-
return JSON.parse(data);
|
|
463
|
-
} catch {
|
|
464
|
-
return {};
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
function writeCache(cacheFilePath, cache) {
|
|
468
|
-
const cacheDirectory = path$1.dirname(cacheFilePath);
|
|
469
|
-
fs$1.mkdirSync(cacheDirectory, { recursive: true });
|
|
470
|
-
fs$1.writeFileSync(cacheFilePath, JSON.stringify(cache, null, 2));
|
|
471
|
-
}
|
|
472
|
-
//#endregion
|
|
473
|
-
//#region src/utils/hash.ts
|
|
474
|
-
function hashBuffer(data) {
|
|
475
|
-
return crypto.createHash("sha256").update(data).digest("hex");
|
|
476
|
-
}
|
|
477
|
-
//#endregion
|
|
478
|
-
//#region src/backends/http-client.ts
|
|
479
|
-
function createFetchClient(defaultHeaders) {
|
|
480
|
-
return { async request(method, url, options) {
|
|
481
|
-
const headers = {
|
|
482
|
-
...defaultHeaders,
|
|
483
|
-
...options?.headers
|
|
484
|
-
};
|
|
485
|
-
const fetchOptions = {
|
|
486
|
-
headers,
|
|
487
|
-
method
|
|
488
|
-
};
|
|
489
|
-
if (options?.body !== void 0) if (options.body instanceof buffer.Buffer) fetchOptions.body = options.body;
|
|
490
|
-
else {
|
|
491
|
-
fetchOptions.body = JSON.stringify(options.body);
|
|
492
|
-
headers["Content-Type"] = "application/json";
|
|
493
|
-
}
|
|
494
|
-
const response = await fetch(url, fetchOptions);
|
|
495
|
-
return {
|
|
496
|
-
body: await (response.headers.get("content-type")?.includes("application/json") ?? false ? response.json() : response.text()),
|
|
497
|
-
headers: { "retry-after": response.headers.get("retry-after") ?? void 0 },
|
|
498
|
-
ok: response.ok,
|
|
499
|
-
status: response.status
|
|
500
|
-
};
|
|
501
|
-
} };
|
|
440
|
+
const configs = (Array.isArray(options) ? options : [options]).map((input) => buildJestArgv(input));
|
|
441
|
+
return test_runner_bundled_default.replace("__CONFIG_JSON__", () => JSON.stringify({ configs }));
|
|
502
442
|
}
|
|
503
443
|
//#endregion
|
|
504
444
|
//#region src/backends/open-cloud.ts
|
|
445
|
+
const PARALLEL_AUTO_CAP = 3;
|
|
505
446
|
const OPEN_CLOUD_BASE_URL = "https://apis.roblox.com";
|
|
506
447
|
const RATE_LIMIT_DEFAULT_WAIT_MS = 5e3;
|
|
507
448
|
const MAX_RATE_LIMIT_RETRIES = 5;
|
|
@@ -511,11 +452,17 @@ const taskStatusResponse = type({
|
|
|
511
452
|
"output?": { "results?": "string[]" },
|
|
512
453
|
"state": "'CANCELLED' | 'COMPLETE' | 'FAILED' | 'PROCESSING'"
|
|
513
454
|
});
|
|
455
|
+
const envelopeSchema$1 = type({ entries: type({
|
|
456
|
+
"elapsedMs?": "number",
|
|
457
|
+
"gameOutput?": "string",
|
|
458
|
+
"jestOutput": "string"
|
|
459
|
+
}).array() });
|
|
514
460
|
var OpenCloudBackend = class {
|
|
515
461
|
credentials;
|
|
516
462
|
http;
|
|
517
463
|
readFile;
|
|
518
464
|
sleepFn;
|
|
465
|
+
kind = "open-cloud";
|
|
519
466
|
constructor(credentials, options) {
|
|
520
467
|
this.credentials = credentials;
|
|
521
468
|
this.http = options?.http ?? createFetchClient({ "x-api-key": credentials.apiKey });
|
|
@@ -527,7 +474,10 @@ var OpenCloudBackend = class {
|
|
|
527
474
|
});
|
|
528
475
|
}
|
|
529
476
|
async runTests(options) {
|
|
530
|
-
const
|
|
477
|
+
const { jobs, parallel } = options;
|
|
478
|
+
if (jobs.length === 0) throw new Error("OpenCloudBackend requires at least one job");
|
|
479
|
+
const primary = jobs[0];
|
|
480
|
+
const placeFilePath = path$1.resolve(primary.config.rootDir, primary.config.placeFile);
|
|
531
481
|
const cacheDirectory = getCacheDirectory();
|
|
532
482
|
const cacheFilePath = path$1.join(cacheDirectory, "upload-cache.json");
|
|
533
483
|
const uploadStart = Date.now();
|
|
@@ -537,30 +487,21 @@ var OpenCloudBackend = class {
|
|
|
537
487
|
const cache = readCache(cacheFilePath);
|
|
538
488
|
const uploadCached = await this.uploadOrReuseCached({
|
|
539
489
|
cache,
|
|
540
|
-
cacheEnabled:
|
|
490
|
+
cacheEnabled: primary.config.cache,
|
|
541
491
|
cacheFilePath,
|
|
542
492
|
cacheKey,
|
|
543
493
|
fileHash,
|
|
544
494
|
placeData
|
|
545
495
|
});
|
|
546
496
|
const uploadMs = Date.now() - uploadStart;
|
|
497
|
+
const buckets = bucketJobs(jobs, parallel);
|
|
547
498
|
const executionStart = Date.now();
|
|
548
|
-
const
|
|
549
|
-
const { gameOutput, jestOutput } = await this.pollForCompletion(taskPath, options.config.timeout, options.config.pollInterval);
|
|
499
|
+
const bucketResults = await Promise.all(buckets.map(async (bucket) => this.runBucket(bucket)));
|
|
550
500
|
const executionMs = Date.now() - executionStart;
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
parsed = parseJestOutput(jestOutput);
|
|
554
|
-
} catch (err) {
|
|
555
|
-
if (err instanceof LuauScriptError) err.gameOutput = gameOutput;
|
|
556
|
-
throw err;
|
|
557
|
-
}
|
|
501
|
+
const flattened = Array.from({ length: jobs.length });
|
|
502
|
+
for (const { indices, results } of bucketResults) for (const [positionInBucket, originalIndex] of indices.entries()) flattened[originalIndex] = results[positionInBucket];
|
|
558
503
|
return {
|
|
559
|
-
|
|
560
|
-
gameOutput,
|
|
561
|
-
luauTiming: parsed.luauTiming,
|
|
562
|
-
result: parsed.result,
|
|
563
|
-
snapshotWrites: parsed.snapshotWrites,
|
|
504
|
+
results: flattened,
|
|
564
505
|
timing: {
|
|
565
506
|
executionMs,
|
|
566
507
|
uploadCached,
|
|
@@ -568,12 +509,12 @@ var OpenCloudBackend = class {
|
|
|
568
509
|
}
|
|
569
510
|
};
|
|
570
511
|
}
|
|
571
|
-
async createExecutionTask(
|
|
512
|
+
async createExecutionTask(inputs, timeoutMs) {
|
|
572
513
|
const url = `${OPEN_CLOUD_BASE_URL}/cloud/v2/universes/${this.credentials.universeId}/places/${this.credentials.placeId}/luau-execution-session-tasks`;
|
|
573
|
-
const script = generateTestScript(
|
|
514
|
+
const script = generateTestScript(inputs);
|
|
574
515
|
const response = await this.http.request("POST", url, { body: {
|
|
575
516
|
script,
|
|
576
|
-
timeout: `${Math.floor(
|
|
517
|
+
timeout: `${Math.floor(timeoutMs / 1e3)}s`
|
|
577
518
|
} });
|
|
578
519
|
if (!response.ok) throw new Error(`Failed to create execution task: ${response.status}`);
|
|
579
520
|
return taskResponse.assert(response.body).path;
|
|
@@ -611,6 +552,26 @@ var OpenCloudBackend = class {
|
|
|
611
552
|
}
|
|
612
553
|
throw new Error("Execution timed out");
|
|
613
554
|
}
|
|
555
|
+
async runBucket(bucket) {
|
|
556
|
+
const { indices, jobs } = bucket;
|
|
557
|
+
const primary = jobs[0];
|
|
558
|
+
const inputs = jobs.map((job) => {
|
|
559
|
+
return {
|
|
560
|
+
config: job.config,
|
|
561
|
+
testFiles: job.testFiles
|
|
562
|
+
};
|
|
563
|
+
});
|
|
564
|
+
const taskPath = await this.createExecutionTask(inputs, primary.config.timeout);
|
|
565
|
+
const { gameOutput, jestOutput } = await this.pollForCompletion(taskPath, primary.config.timeout, primary.config.pollInterval);
|
|
566
|
+
const entries = parseEnvelope(jestOutput);
|
|
567
|
+
if (entries.length !== jobs.length) throw new Error(`Open Cloud backend returned ${entries.length.toString()} entries but bucket had ${jobs.length.toString()} jobs`);
|
|
568
|
+
return {
|
|
569
|
+
indices,
|
|
570
|
+
results: entries.map((entry, index) => {
|
|
571
|
+
return buildProjectResult(entry, jobs[index], gameOutput);
|
|
572
|
+
})
|
|
573
|
+
};
|
|
574
|
+
}
|
|
614
575
|
async uploadOrReuseCached({ cache, cacheEnabled, cacheFilePath, cacheKey, fileHash, placeData }) {
|
|
615
576
|
if (cacheEnabled && isUploaded(cache, cacheKey, fileHash)) return true;
|
|
616
577
|
await this.uploadPlaceData(placeData);
|
|
@@ -640,6 +601,55 @@ function createOpenCloudBackend() {
|
|
|
640
601
|
universeId
|
|
641
602
|
});
|
|
642
603
|
}
|
|
604
|
+
function resolveBucketCount(parallel, jobCount) {
|
|
605
|
+
if (parallel === void 0) return 1;
|
|
606
|
+
if (parallel === "auto") return Math.min(jobCount, PARALLEL_AUTO_CAP);
|
|
607
|
+
if (parallel < 1) throw new Error(`--parallel must be >= 1, got ${parallel.toString()}`);
|
|
608
|
+
return Math.min(Math.floor(parallel), jobCount);
|
|
609
|
+
}
|
|
610
|
+
function bucketJobs(jobs, parallel) {
|
|
611
|
+
const bucketCount = resolveBucketCount(parallel, jobs.length);
|
|
612
|
+
const buckets = [];
|
|
613
|
+
for (let index = 0; index < bucketCount; index++) buckets.push({
|
|
614
|
+
indices: [],
|
|
615
|
+
jobs: []
|
|
616
|
+
});
|
|
617
|
+
for (const [originalIndex, job] of jobs.entries()) {
|
|
618
|
+
const bucket = buckets[originalIndex % bucketCount];
|
|
619
|
+
bucket.indices.push(originalIndex);
|
|
620
|
+
bucket.jobs.push(job);
|
|
621
|
+
}
|
|
622
|
+
return buckets;
|
|
623
|
+
}
|
|
624
|
+
function parseEnvelope(jestOutput) {
|
|
625
|
+
const envelope = envelopeSchema$1(JSON.parse(jestOutput));
|
|
626
|
+
if (envelope instanceof type.errors) return [{
|
|
627
|
+
elapsedMs: 0,
|
|
628
|
+
jestOutput
|
|
629
|
+
}];
|
|
630
|
+
return envelope.entries;
|
|
631
|
+
}
|
|
632
|
+
function buildProjectResult(entry, job, fallbackGameOutput) {
|
|
633
|
+
const gameOutput = entry.gameOutput ?? fallbackGameOutput;
|
|
634
|
+
let parsed;
|
|
635
|
+
try {
|
|
636
|
+
parsed = parseJestOutput(entry.jestOutput);
|
|
637
|
+
} catch (err) {
|
|
638
|
+
if (err instanceof LuauScriptError) err.gameOutput = gameOutput;
|
|
639
|
+
throw err;
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
coverageData: parsed.coverageData,
|
|
643
|
+
displayColor: job.displayColor,
|
|
644
|
+
displayName: job.displayName,
|
|
645
|
+
elapsedMs: entry.elapsedMs ?? 0,
|
|
646
|
+
gameOutput,
|
|
647
|
+
luauTiming: parsed.luauTiming,
|
|
648
|
+
result: parsed.result,
|
|
649
|
+
setupMs: parsed.setupSeconds !== void 0 ? Math.round(parsed.setupSeconds * 1e3) : void 0,
|
|
650
|
+
snapshotWrites: parsed.snapshotWrites
|
|
651
|
+
};
|
|
652
|
+
}
|
|
643
653
|
function parseRetryAfter(headers) {
|
|
644
654
|
const value = headers?.["retry-after"];
|
|
645
655
|
if (value === void 0) return RATE_LIMIT_DEFAULT_WAIT_MS;
|
|
@@ -650,6 +660,11 @@ function parseRetryAfter(headers) {
|
|
|
650
660
|
//#endregion
|
|
651
661
|
//#region src/backends/studio.ts
|
|
652
662
|
const DEFAULT_STUDIO_TIMEOUT = 3e5;
|
|
663
|
+
const envelopeSchema = type({ entries: type({
|
|
664
|
+
"elapsedMs?": "number",
|
|
665
|
+
"gameOutput?": "string",
|
|
666
|
+
"jestOutput": "string"
|
|
667
|
+
}).array() });
|
|
653
668
|
const pluginMessageSchema = type({
|
|
654
669
|
"gameOutput?": "string",
|
|
655
670
|
"jestOutput": "string",
|
|
@@ -661,45 +676,72 @@ var StudioBackend = class {
|
|
|
661
676
|
port;
|
|
662
677
|
timeout;
|
|
663
678
|
preConnected;
|
|
679
|
+
wss;
|
|
680
|
+
kind = "studio";
|
|
664
681
|
constructor(options) {
|
|
665
682
|
this.port = options.port;
|
|
666
683
|
this.timeout = options.timeout ?? DEFAULT_STUDIO_TIMEOUT;
|
|
667
684
|
this.createServer = options.createServer ?? ((port) => new WebSocketServer({ port }));
|
|
668
685
|
this.preConnected = options.preConnected;
|
|
669
686
|
}
|
|
687
|
+
close() {
|
|
688
|
+
const server = this.wss;
|
|
689
|
+
this.wss = void 0;
|
|
690
|
+
if (server === void 0) return;
|
|
691
|
+
server.close();
|
|
692
|
+
}
|
|
670
693
|
async runTests(options) {
|
|
671
694
|
const pre = this.preConnected;
|
|
672
695
|
this.preConnected = void 0;
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
return await this.executeViaPlugin(wss, options, pre?.socket);
|
|
676
|
-
} finally {
|
|
677
|
-
wss.close();
|
|
678
|
-
}
|
|
696
|
+
this.wss ??= pre?.server ?? this.createServer(this.port);
|
|
697
|
+
return this.executeViaPlugin(this.wss, options.jobs, pre?.socket);
|
|
679
698
|
}
|
|
680
|
-
|
|
681
|
-
const
|
|
682
|
-
const config = buildJestArgv(options);
|
|
683
|
-
const executionStart = Date.now();
|
|
684
|
-
const message = await this.waitForResult(wss, requestId, config, existingSocket);
|
|
685
|
-
const executionMs = Date.now() - executionStart;
|
|
699
|
+
buildProjectResult(entry, job, fallbackGameOutput) {
|
|
700
|
+
const gameOutput = entry.gameOutput ?? fallbackGameOutput;
|
|
686
701
|
let parsed;
|
|
687
702
|
try {
|
|
688
|
-
parsed = parseJestOutput(
|
|
703
|
+
parsed = parseJestOutput(entry.jestOutput);
|
|
689
704
|
} catch (err) {
|
|
690
|
-
if (err instanceof LuauScriptError) err.gameOutput =
|
|
705
|
+
if (err instanceof LuauScriptError) err.gameOutput = gameOutput;
|
|
691
706
|
throw err;
|
|
692
707
|
}
|
|
693
708
|
return {
|
|
694
709
|
coverageData: parsed.coverageData,
|
|
695
|
-
|
|
710
|
+
displayColor: job.displayColor,
|
|
711
|
+
displayName: job.displayName,
|
|
712
|
+
elapsedMs: entry.elapsedMs ?? 0,
|
|
713
|
+
gameOutput,
|
|
696
714
|
luauTiming: parsed.luauTiming,
|
|
697
715
|
result: parsed.result,
|
|
698
|
-
|
|
716
|
+
setupMs: parsed.setupSeconds !== void 0 ? Math.round(parsed.setupSeconds * 1e3) : void 0,
|
|
717
|
+
snapshotWrites: parsed.snapshotWrites
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
async executeViaPlugin(wss, jobs, existingSocket) {
|
|
721
|
+
const requestId = randomUUID();
|
|
722
|
+
const configs = jobs.map((job) => buildJestArgv(job));
|
|
723
|
+
const executionStart = Date.now();
|
|
724
|
+
const message = await this.waitForResult(wss, requestId, configs, existingSocket);
|
|
725
|
+
const executionMs = Date.now() - executionStart;
|
|
726
|
+
const entries = this.parseEnvelope(message.jestOutput);
|
|
727
|
+
if (entries.length !== jobs.length) throw new Error(`Studio backend returned ${entries.length.toString()} entries but request had ${jobs.length.toString()} jobs`);
|
|
728
|
+
return {
|
|
729
|
+
results: entries.map((entry, index) => {
|
|
730
|
+
const matched = jobs[index];
|
|
731
|
+
return this.buildProjectResult(entry, matched, message.gameOutput);
|
|
732
|
+
}),
|
|
699
733
|
timing: { executionMs }
|
|
700
734
|
};
|
|
701
735
|
}
|
|
702
|
-
|
|
736
|
+
parseEnvelope(jestOutput) {
|
|
737
|
+
const envelope = envelopeSchema(JSON.parse(jestOutput));
|
|
738
|
+
if (envelope instanceof type.errors) return [{
|
|
739
|
+
elapsedMs: 0,
|
|
740
|
+
jestOutput
|
|
741
|
+
}];
|
|
742
|
+
return envelope.entries;
|
|
743
|
+
}
|
|
744
|
+
async waitForResult(wss, requestId, configs, existingSocket) {
|
|
703
745
|
return new Promise((resolve, reject) => {
|
|
704
746
|
const timer = setTimeout(() => {
|
|
705
747
|
reject(/* @__PURE__ */ new Error("Timed out waiting for Studio plugin connection"));
|
|
@@ -707,7 +749,7 @@ var StudioBackend = class {
|
|
|
707
749
|
function attachSocket(ws) {
|
|
708
750
|
ws.send(JSON.stringify({
|
|
709
751
|
action: "run_tests",
|
|
710
|
-
config,
|
|
752
|
+
config: { configs },
|
|
711
753
|
request_id: requestId
|
|
712
754
|
}));
|
|
713
755
|
ws.on("message", (data) => {
|
|
@@ -1058,6 +1100,48 @@ function createSourceMapper(config) {
|
|
|
1058
1100
|
}
|
|
1059
1101
|
};
|
|
1060
1102
|
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Compose multiple `SourceMapper`s into one that tries every child in order.
|
|
1105
|
+
* Used by the multi-project CLI path so that failure messages and GitHub
|
|
1106
|
+
* annotations can resolve frames from any project's TS/Luau mapping.
|
|
1107
|
+
*
|
|
1108
|
+
* Each child mapper only rewrites frames it can resolve, leaving the rest
|
|
1109
|
+
* untouched. Chaining `mapFailureMessage` / `mapFailureWithLocations` calls
|
|
1110
|
+
* through every child is therefore safe: later mappers see the partially
|
|
1111
|
+
* rewritten string and still parse any remaining `[string "..."]` frames.
|
|
1112
|
+
* Locations accumulate across mappers; `resolveTestFilePath` returns the
|
|
1113
|
+
* first child's hit.
|
|
1114
|
+
*/
|
|
1115
|
+
function combineSourceMappers(mappers) {
|
|
1116
|
+
if (mappers.length === 0) return;
|
|
1117
|
+
if (mappers.length === 1) return mappers[0];
|
|
1118
|
+
return {
|
|
1119
|
+
mapFailureMessage(message) {
|
|
1120
|
+
let result = message;
|
|
1121
|
+
for (const mapper of mappers) result = mapper.mapFailureMessage(result);
|
|
1122
|
+
return result;
|
|
1123
|
+
},
|
|
1124
|
+
mapFailureWithLocations(message) {
|
|
1125
|
+
let mappedMessage = message;
|
|
1126
|
+
const locations = [];
|
|
1127
|
+
for (const mapper of mappers) {
|
|
1128
|
+
const partial = mapper.mapFailureWithLocations(mappedMessage);
|
|
1129
|
+
mappedMessage = partial.message;
|
|
1130
|
+
locations.push(...partial.locations);
|
|
1131
|
+
}
|
|
1132
|
+
return {
|
|
1133
|
+
locations,
|
|
1134
|
+
message: mappedMessage
|
|
1135
|
+
};
|
|
1136
|
+
},
|
|
1137
|
+
resolveTestFilePath(testFilePath) {
|
|
1138
|
+
for (const mapper of mappers) {
|
|
1139
|
+
const resolved = mapper.resolveTestFilePath(testFilePath);
|
|
1140
|
+
if (resolved !== void 0) return resolved;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1061
1145
|
function getSourceSnippet({ column, context = 2, filePath, line, sourceContent }) {
|
|
1062
1146
|
const content = sourceContent ?? (fs$1.existsSync(filePath) ? fs$1.readFileSync(filePath, "utf-8") : void 0);
|
|
1063
1147
|
if (content === void 0) return;
|
|
@@ -1410,13 +1494,16 @@ function formatTestSummary(result, timing, styles, options) {
|
|
|
1410
1494
|
}
|
|
1411
1495
|
const startAtStr = new Date(timing.startTime).toLocaleTimeString("en-GB", { hour12: false });
|
|
1412
1496
|
lines.push(`${st.dim(" Start at")} ${startAtStr}`);
|
|
1413
|
-
const
|
|
1497
|
+
const setupMs = timing.setupMs ?? 0;
|
|
1498
|
+
const environmentMs = Math.max(0, timing.executionMs - timing.testsMs - setupMs);
|
|
1414
1499
|
const uploadMs = timing.uploadMs ?? 0;
|
|
1415
1500
|
const coverageMs = timing.coverageMs ?? 0;
|
|
1416
1501
|
const cliMs = Math.max(0, timing.totalMs - uploadMs - timing.executionMs - coverageMs);
|
|
1417
1502
|
const breakdownParts = [];
|
|
1418
1503
|
if (timing.uploadMs !== void 0) breakdownParts.push(timing.uploadCached === true ? `upload ${timing.uploadMs}ms (cached)` : `upload ${timing.uploadMs}ms`);
|
|
1419
|
-
breakdownParts.push(`environment ${environmentMs}ms
|
|
1504
|
+
breakdownParts.push(`environment ${environmentMs}ms`);
|
|
1505
|
+
if (setupMs > 0) breakdownParts.push(`setup ${setupMs}ms`);
|
|
1506
|
+
breakdownParts.push(`tests ${timing.testsMs}ms`, `cli ${cliMs}ms`);
|
|
1420
1507
|
if (coverageMs > 0) breakdownParts.push(`coverage ${coverageMs}ms`);
|
|
1421
1508
|
const breakdown = st.dim(`(${breakdownParts.join(", ")})`);
|
|
1422
1509
|
lines.push(`${st.dim(" Duration")} ${timing.totalMs}ms ${breakdown}`);
|
|
@@ -2359,52 +2446,6 @@ const rojoProjectSchema = type({
|
|
|
2359
2446
|
"tree": "object"
|
|
2360
2447
|
}).as();
|
|
2361
2448
|
//#endregion
|
|
2362
|
-
//#region src/utils/rojo-tree.ts
|
|
2363
|
-
function resolveNestedProjects(tree, rootDirectory) {
|
|
2364
|
-
return resolveTree(tree, rootDirectory, rootDirectory, /* @__PURE__ */ new Set());
|
|
2365
|
-
}
|
|
2366
|
-
function collectPaths(node, result) {
|
|
2367
|
-
for (const [key, value] of Object.entries(node)) if (key === "$path" && typeof value === "string") result.push(value.replaceAll("\\", "/"));
|
|
2368
|
-
else if (typeof value === "object" && !Array.isArray(value) && !key.startsWith("$")) collectPaths(value, result);
|
|
2369
|
-
}
|
|
2370
|
-
function inlineNestedProject(projectPath, currentDirectory, originalRoot, visited) {
|
|
2371
|
-
const chain = new Set(visited);
|
|
2372
|
-
chain.add(projectPath);
|
|
2373
|
-
let content;
|
|
2374
|
-
try {
|
|
2375
|
-
content = readFileSync(projectPath, "utf-8");
|
|
2376
|
-
} catch (err) {
|
|
2377
|
-
const relativePath = relative(currentDirectory, projectPath);
|
|
2378
|
-
throw new Error(`Could not read nested Rojo project: ${relativePath}`, { cause: err });
|
|
2379
|
-
}
|
|
2380
|
-
return resolveTree(JSON.parse(content).tree, dirname(projectPath), originalRoot, chain);
|
|
2381
|
-
}
|
|
2382
|
-
function resolveRootRelativePath(currentDirectory, value, originalRoot) {
|
|
2383
|
-
return relative(originalRoot, join(currentDirectory, value)).replaceAll("\\", "/");
|
|
2384
|
-
}
|
|
2385
|
-
function resolveTree(node, currentDirectory, originalRoot, visited) {
|
|
2386
|
-
const resolved = {};
|
|
2387
|
-
for (const [key, value] of Object.entries(node)) {
|
|
2388
|
-
if (key === "$path" && typeof value === "string" && value.endsWith(".project.json")) {
|
|
2389
|
-
const projectPath = join(currentDirectory, value);
|
|
2390
|
-
if (visited.has(projectPath)) throw new Error(`Circular project reference: ${value}`);
|
|
2391
|
-
const innerTree = inlineNestedProject(projectPath, currentDirectory, originalRoot, visited);
|
|
2392
|
-
for (const [innerKey, innerValue] of Object.entries(innerTree)) resolved[innerKey] = innerValue;
|
|
2393
|
-
continue;
|
|
2394
|
-
}
|
|
2395
|
-
if (key === "$path" && typeof value === "string") {
|
|
2396
|
-
resolved[key] = resolveRootRelativePath(currentDirectory, value, originalRoot);
|
|
2397
|
-
continue;
|
|
2398
|
-
}
|
|
2399
|
-
if (key.startsWith("$") || typeof value !== "object" || Array.isArray(value)) {
|
|
2400
|
-
resolved[key] = value;
|
|
2401
|
-
continue;
|
|
2402
|
-
}
|
|
2403
|
-
resolved[key] = resolveTree(value, currentDirectory, originalRoot, visited);
|
|
2404
|
-
}
|
|
2405
|
-
return resolved;
|
|
2406
|
-
}
|
|
2407
|
-
//#endregion
|
|
2408
2449
|
//#region src/executor.ts
|
|
2409
2450
|
function isLuauProject(testFiles, tsconfigMappings) {
|
|
2410
2451
|
if (tsconfigMappings.length > 0) return false;
|
|
@@ -2477,15 +2518,40 @@ function formatExecuteOutput(options) {
|
|
|
2477
2518
|
version
|
|
2478
2519
|
});
|
|
2479
2520
|
}
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2521
|
+
/**
|
|
2522
|
+
* Build a `ProjectJob` with `snapshotFormat` resolved per-project. Each job
|
|
2523
|
+
* carries its own config so the Luau runner never re-resolves or shares format
|
|
2524
|
+
* state across projects (fixes the spike's snapshot-diff regression — C1).
|
|
2525
|
+
*/
|
|
2526
|
+
function buildProjectJob(parameters) {
|
|
2527
|
+
const tsconfigMappings = resolveAllTsconfigMappings(parameters.config.rootDir);
|
|
2528
|
+
const luauProject = isLuauProject(parameters.testFiles, tsconfigMappings);
|
|
2529
|
+
return {
|
|
2530
|
+
config: applySnapshotFormatDefaults(parameters.config, luauProject),
|
|
2531
|
+
displayColor: parameters.displayColor,
|
|
2532
|
+
displayName: parameters.displayName ?? "",
|
|
2533
|
+
testFiles: parameters.testFiles
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
/**
|
|
2537
|
+
* Thin wrapper over `backend.runTests`. Fires exactly once per CLI invocation
|
|
2538
|
+
* with a full `ProjectJob[]` envelope. Returns the raw `BackendResult`.
|
|
2539
|
+
*/
|
|
2540
|
+
async function executeBackend(backend, jobs, parallel) {
|
|
2541
|
+
return backend.runTests({
|
|
2542
|
+
jobs,
|
|
2543
|
+
parallel
|
|
2488
2544
|
});
|
|
2545
|
+
}
|
|
2546
|
+
/**
|
|
2547
|
+
* Process a single `ProjectBackendResult` into an `ExecuteResult`: writes
|
|
2548
|
+
* snapshots, builds the source mapper, resolves test-file paths, and renders
|
|
2549
|
+
* formatter output. Called once per job.
|
|
2550
|
+
*/
|
|
2551
|
+
function processProjectResult(entry, options) {
|
|
2552
|
+
const { backendTiming, config, deferFormatting, startTime, version } = options;
|
|
2553
|
+
const { coverageData, gameOutput, luauTiming, result, setupMs, snapshotWrites } = entry;
|
|
2554
|
+
const tsconfigMappings = resolveAllTsconfigMappings(config.rootDir);
|
|
2489
2555
|
if (snapshotWrites !== void 0) writeSnapshots(snapshotWrites, config, tsconfigMappings);
|
|
2490
2556
|
const testsMs = calculateTestsMs(result.testResults);
|
|
2491
2557
|
const sourceMapper = config.sourceMap ? buildSourceMapper(config, tsconfigMappings) : void 0;
|
|
@@ -2493,18 +2559,19 @@ async function execute(options) {
|
|
|
2493
2559
|
const totalMs = Date.now() - startTime;
|
|
2494
2560
|
const timing = {
|
|
2495
2561
|
executionMs: backendTiming.executionMs,
|
|
2562
|
+
setupMs,
|
|
2496
2563
|
startTime,
|
|
2497
2564
|
testsMs,
|
|
2498
2565
|
totalMs,
|
|
2499
2566
|
uploadCached: backendTiming.uploadCached,
|
|
2500
2567
|
uploadMs: backendTiming.uploadMs
|
|
2501
2568
|
};
|
|
2502
|
-
const output =
|
|
2569
|
+
const output = deferFormatting !== true ? formatExecuteOutput({
|
|
2503
2570
|
config,
|
|
2504
2571
|
result,
|
|
2505
2572
|
sourceMapper,
|
|
2506
2573
|
timing,
|
|
2507
|
-
version
|
|
2574
|
+
version
|
|
2508
2575
|
}) : "";
|
|
2509
2576
|
if (luauTiming !== void 0) printLuauTiming(luauTiming);
|
|
2510
2577
|
return {
|
|
@@ -2517,6 +2584,28 @@ async function execute(options) {
|
|
|
2517
2584
|
timing
|
|
2518
2585
|
};
|
|
2519
2586
|
}
|
|
2587
|
+
/**
|
|
2588
|
+
* Single-project convenience wrapper: builds a length-1 jobs array, fires
|
|
2589
|
+
* `executeBackend` once, and maps the single entry through
|
|
2590
|
+
* `processProjectResult`. Multi-project callers drive `executeBackend` +
|
|
2591
|
+
* `processProjectResult` directly from `cli.ts`.
|
|
2592
|
+
*/
|
|
2593
|
+
async function execute(options) {
|
|
2594
|
+
const startTime = Date.now();
|
|
2595
|
+
const job = buildProjectJob({
|
|
2596
|
+
config: options.config,
|
|
2597
|
+
testFiles: options.testFiles
|
|
2598
|
+
});
|
|
2599
|
+
const { results, timing: backendTiming } = await executeBackend(options.backend, [job]);
|
|
2600
|
+
const first = results[0];
|
|
2601
|
+
return processProjectResult(first, {
|
|
2602
|
+
backendTiming,
|
|
2603
|
+
config: job.config,
|
|
2604
|
+
deferFormatting: options.deferFormatting,
|
|
2605
|
+
startTime,
|
|
2606
|
+
version: options.version
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2520
2609
|
function normalizeDirectoryPath(directory) {
|
|
2521
2610
|
return path$1.normalize(directory).replaceAll("\\", "/");
|
|
2522
2611
|
}
|
|
@@ -2672,6 +2761,276 @@ function writeSnapshots(snapshotWrites, config, tsconfigMappings) {
|
|
|
2672
2761
|
}
|
|
2673
2762
|
}
|
|
2674
2763
|
//#endregion
|
|
2764
|
+
//#region packages/luau-ast/dist/index.mjs
|
|
2765
|
+
function visitExpression(expression, visitor) {
|
|
2766
|
+
if (visitor.visitExpr?.(expression) === false) return;
|
|
2767
|
+
const { tag } = expression;
|
|
2768
|
+
switch (tag) {
|
|
2769
|
+
case "binary":
|
|
2770
|
+
visitExprBinary(expression, visitor);
|
|
2771
|
+
break;
|
|
2772
|
+
case "boolean":
|
|
2773
|
+
visitor.visitExprConstantBool?.(expression);
|
|
2774
|
+
break;
|
|
2775
|
+
case "call":
|
|
2776
|
+
visitExprCall(expression, visitor);
|
|
2777
|
+
break;
|
|
2778
|
+
case "cast":
|
|
2779
|
+
visitExprTypeAssertion(expression, visitor);
|
|
2780
|
+
break;
|
|
2781
|
+
case "conditional":
|
|
2782
|
+
visitExprIfElse(expression, visitor);
|
|
2783
|
+
break;
|
|
2784
|
+
case "function":
|
|
2785
|
+
visitExprFunction(expression, visitor);
|
|
2786
|
+
break;
|
|
2787
|
+
case "global":
|
|
2788
|
+
visitor.visitExprGlobal?.(expression);
|
|
2789
|
+
break;
|
|
2790
|
+
case "group":
|
|
2791
|
+
visitExprGroup(expression, visitor);
|
|
2792
|
+
break;
|
|
2793
|
+
case "index":
|
|
2794
|
+
visitExprIndexExpr(expression, visitor);
|
|
2795
|
+
break;
|
|
2796
|
+
case "indexname":
|
|
2797
|
+
visitExprIndexName(expression, visitor);
|
|
2798
|
+
break;
|
|
2799
|
+
case "instantiate":
|
|
2800
|
+
visitExprInstantiate(expression, visitor);
|
|
2801
|
+
break;
|
|
2802
|
+
case "interpolatedstring":
|
|
2803
|
+
visitExprInterpString(expression, visitor);
|
|
2804
|
+
break;
|
|
2805
|
+
case "local":
|
|
2806
|
+
visitor.visitExprLocal?.(expression);
|
|
2807
|
+
break;
|
|
2808
|
+
case "nil":
|
|
2809
|
+
visitor.visitExprConstantNil?.(expression);
|
|
2810
|
+
break;
|
|
2811
|
+
case "number":
|
|
2812
|
+
visitor.visitExprConstantNumber?.(expression);
|
|
2813
|
+
break;
|
|
2814
|
+
case "string":
|
|
2815
|
+
visitor.visitExprConstantString?.(expression);
|
|
2816
|
+
break;
|
|
2817
|
+
case "table":
|
|
2818
|
+
visitExprTable(expression, visitor);
|
|
2819
|
+
break;
|
|
2820
|
+
case "unary":
|
|
2821
|
+
visitExprUnary(expression, visitor);
|
|
2822
|
+
break;
|
|
2823
|
+
case "vararg":
|
|
2824
|
+
visitor.visitExprVarargs?.(expression);
|
|
2825
|
+
break;
|
|
2826
|
+
default: break;
|
|
2827
|
+
}
|
|
2828
|
+
visitor.visitExprEnd?.(expression);
|
|
2829
|
+
}
|
|
2830
|
+
function visitStatement(statement, visitor) {
|
|
2831
|
+
const { tag } = statement;
|
|
2832
|
+
switch (tag) {
|
|
2833
|
+
case "assign":
|
|
2834
|
+
visitStatAssign(statement, visitor);
|
|
2835
|
+
break;
|
|
2836
|
+
case "block":
|
|
2837
|
+
visitStatBlock(statement, visitor);
|
|
2838
|
+
break;
|
|
2839
|
+
case "break":
|
|
2840
|
+
visitor.visitStatBreak?.(statement);
|
|
2841
|
+
break;
|
|
2842
|
+
case "compoundassign":
|
|
2843
|
+
visitStatCompoundAssign(statement, visitor);
|
|
2844
|
+
break;
|
|
2845
|
+
case "conditional":
|
|
2846
|
+
visitStatIf(statement, visitor);
|
|
2847
|
+
break;
|
|
2848
|
+
case "continue":
|
|
2849
|
+
visitor.visitStatContinue?.(statement);
|
|
2850
|
+
break;
|
|
2851
|
+
case "do":
|
|
2852
|
+
visitStatDo(statement, visitor);
|
|
2853
|
+
break;
|
|
2854
|
+
case "expression":
|
|
2855
|
+
visitStatExpr(statement, visitor);
|
|
2856
|
+
break;
|
|
2857
|
+
case "for":
|
|
2858
|
+
visitStatFor(statement, visitor);
|
|
2859
|
+
break;
|
|
2860
|
+
case "forin":
|
|
2861
|
+
visitStatForIn(statement, visitor);
|
|
2862
|
+
break;
|
|
2863
|
+
case "function":
|
|
2864
|
+
visitStatFunction(statement, visitor);
|
|
2865
|
+
break;
|
|
2866
|
+
case "local":
|
|
2867
|
+
visitStatLocal(statement, visitor);
|
|
2868
|
+
break;
|
|
2869
|
+
case "localfunction":
|
|
2870
|
+
visitStatLocalFunction(statement, visitor);
|
|
2871
|
+
break;
|
|
2872
|
+
case "repeat":
|
|
2873
|
+
visitStatRepeat(statement, visitor);
|
|
2874
|
+
break;
|
|
2875
|
+
case "return":
|
|
2876
|
+
visitStatReturn(statement, visitor);
|
|
2877
|
+
break;
|
|
2878
|
+
case "typealias":
|
|
2879
|
+
visitor.visitStatTypeAlias?.(statement);
|
|
2880
|
+
break;
|
|
2881
|
+
case "typefunction":
|
|
2882
|
+
visitor.visitStatTypeFunction?.(statement);
|
|
2883
|
+
break;
|
|
2884
|
+
case "while":
|
|
2885
|
+
visitStatWhile(statement, visitor);
|
|
2886
|
+
break;
|
|
2887
|
+
default: break;
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
function visitBlock(block, visitor) {
|
|
2891
|
+
visitStatBlock(block, visitor);
|
|
2892
|
+
}
|
|
2893
|
+
function visitPunctuated(list, visitor, apply) {
|
|
2894
|
+
for (const item of list) apply(item.node, visitor);
|
|
2895
|
+
}
|
|
2896
|
+
function visitStatBlock(block, visitor) {
|
|
2897
|
+
if (visitor.visitStatBlock?.(block) === false) return;
|
|
2898
|
+
for (const statement of block.statements) visitStatement(statement, visitor);
|
|
2899
|
+
visitor.visitStatBlockEnd?.(block);
|
|
2900
|
+
}
|
|
2901
|
+
function visitStatDo(node, visitor) {
|
|
2902
|
+
if (visitor.visitStatDo?.(node) === false) return;
|
|
2903
|
+
visitStatBlock(node.body, visitor);
|
|
2904
|
+
}
|
|
2905
|
+
function visitStatIf(node, visitor) {
|
|
2906
|
+
if (visitor.visitStatIf?.(node) === false) return;
|
|
2907
|
+
visitExpression(node.condition, visitor);
|
|
2908
|
+
visitStatBlock(node.thenBlock, visitor);
|
|
2909
|
+
for (const elseif of node.elseifs) visitElseIfStat(elseif, visitor);
|
|
2910
|
+
if (node.elseBlock) visitStatBlock(node.elseBlock, visitor);
|
|
2911
|
+
}
|
|
2912
|
+
function visitElseIfStat(node, visitor) {
|
|
2913
|
+
visitExpression(node.condition, visitor);
|
|
2914
|
+
visitStatBlock(node.thenBlock, visitor);
|
|
2915
|
+
}
|
|
2916
|
+
function visitStatWhile(node, visitor) {
|
|
2917
|
+
if (visitor.visitStatWhile?.(node) === false) return;
|
|
2918
|
+
visitExpression(node.condition, visitor);
|
|
2919
|
+
visitStatBlock(node.body, visitor);
|
|
2920
|
+
}
|
|
2921
|
+
function visitStatRepeat(node, visitor) {
|
|
2922
|
+
if (visitor.visitStatRepeat?.(node) === false) return;
|
|
2923
|
+
visitStatBlock(node.body, visitor);
|
|
2924
|
+
visitExpression(node.condition, visitor);
|
|
2925
|
+
}
|
|
2926
|
+
function visitStatReturn(node, visitor) {
|
|
2927
|
+
if (visitor.visitStatReturn?.(node) === false) return;
|
|
2928
|
+
visitPunctuated(node.expressions, visitor, visitExpression);
|
|
2929
|
+
}
|
|
2930
|
+
function visitStatLocal(node, visitor) {
|
|
2931
|
+
if (visitor.visitStatLocal?.(node) === false) return;
|
|
2932
|
+
visitPunctuated(node.values, visitor, visitExpression);
|
|
2933
|
+
}
|
|
2934
|
+
function visitStatFor(node, visitor) {
|
|
2935
|
+
if (visitor.visitStatFor?.(node) === false) return;
|
|
2936
|
+
visitExpression(node.from, visitor);
|
|
2937
|
+
visitExpression(node.to, visitor);
|
|
2938
|
+
if (node.step) visitExpression(node.step, visitor);
|
|
2939
|
+
visitStatBlock(node.body, visitor);
|
|
2940
|
+
}
|
|
2941
|
+
function visitStatForIn(node, visitor) {
|
|
2942
|
+
if (visitor.visitStatForIn?.(node) === false) return;
|
|
2943
|
+
visitPunctuated(node.values, visitor, visitExpression);
|
|
2944
|
+
visitStatBlock(node.body, visitor);
|
|
2945
|
+
}
|
|
2946
|
+
function visitStatAssign(node, visitor) {
|
|
2947
|
+
if (visitor.visitStatAssign?.(node) === false) return;
|
|
2948
|
+
visitPunctuated(node.variables, visitor, visitExpression);
|
|
2949
|
+
visitPunctuated(node.values, visitor, visitExpression);
|
|
2950
|
+
}
|
|
2951
|
+
function visitStatCompoundAssign(node, visitor) {
|
|
2952
|
+
if (visitor.visitStatCompoundAssign?.(node) === false) return;
|
|
2953
|
+
visitExpression(node.variable, visitor);
|
|
2954
|
+
visitExpression(node.value, visitor);
|
|
2955
|
+
}
|
|
2956
|
+
function visitStatExpr(node, visitor) {
|
|
2957
|
+
if (visitor.visitStatExpr?.(node) === false) return;
|
|
2958
|
+
visitExpression(node.expression, visitor);
|
|
2959
|
+
}
|
|
2960
|
+
function visitStatFunction(node, visitor) {
|
|
2961
|
+
if (visitor.visitStatFunction?.(node) === false) return;
|
|
2962
|
+
visitExpression(node.name, visitor);
|
|
2963
|
+
visitExprFunction(node.func, visitor);
|
|
2964
|
+
}
|
|
2965
|
+
function visitStatLocalFunction(node, visitor) {
|
|
2966
|
+
if (visitor.visitStatLocalFunction?.(node) === false) return;
|
|
2967
|
+
visitExprFunction(node.func, visitor);
|
|
2968
|
+
}
|
|
2969
|
+
function visitExprFunction(node, visitor) {
|
|
2970
|
+
if (visitor.visitExprFunction?.(node) === false) return;
|
|
2971
|
+
visitStatBlock(node.body, visitor);
|
|
2972
|
+
visitor.visitExprFunctionEnd?.(node);
|
|
2973
|
+
}
|
|
2974
|
+
function visitExprCall(node, visitor) {
|
|
2975
|
+
if (visitor.visitExprCall?.(node) === false) return;
|
|
2976
|
+
visitExpression(node.func, visitor);
|
|
2977
|
+
visitPunctuated(node.arguments, visitor, visitExpression);
|
|
2978
|
+
}
|
|
2979
|
+
function visitExprUnary(node, visitor) {
|
|
2980
|
+
if (visitor.visitExprUnary?.(node) === false) return;
|
|
2981
|
+
visitExpression(node.operand, visitor);
|
|
2982
|
+
}
|
|
2983
|
+
function visitExprBinary(node, visitor) {
|
|
2984
|
+
if (visitor.visitExprBinary?.(node) === false) return;
|
|
2985
|
+
visitExpression(node.lhsOperand, visitor);
|
|
2986
|
+
visitExpression(node.rhsOperand, visitor);
|
|
2987
|
+
}
|
|
2988
|
+
function visitExprTable(node, visitor) {
|
|
2989
|
+
if (visitor.visitExprTable?.(node) === false) return;
|
|
2990
|
+
for (const item of node.entries) visitTableExprItem(item, visitor);
|
|
2991
|
+
}
|
|
2992
|
+
function visitTableExprItem(node, visitor) {
|
|
2993
|
+
if (visitor.visitTableExprItem?.(node) === false) return;
|
|
2994
|
+
visitExpression(node.value, visitor);
|
|
2995
|
+
if (node.kind === "general") visitExpression(node.key, visitor);
|
|
2996
|
+
}
|
|
2997
|
+
function visitExprIndexName(node, visitor) {
|
|
2998
|
+
if (visitor.visitExprIndexName?.(node) === false) return;
|
|
2999
|
+
visitExpression(node.expression, visitor);
|
|
3000
|
+
}
|
|
3001
|
+
function visitExprIndexExpr(node, visitor) {
|
|
3002
|
+
if (visitor.visitExprIndexExpr?.(node) === false) return;
|
|
3003
|
+
visitExpression(node.expression, visitor);
|
|
3004
|
+
visitExpression(node.index, visitor);
|
|
3005
|
+
}
|
|
3006
|
+
function visitExprGroup(node, visitor) {
|
|
3007
|
+
if (visitor.visitExprGroup?.(node) === false) return;
|
|
3008
|
+
visitExpression(node.expression, visitor);
|
|
3009
|
+
}
|
|
3010
|
+
function visitExprInterpString(node, visitor) {
|
|
3011
|
+
if (visitor.visitExprInterpString?.(node) === false) return;
|
|
3012
|
+
for (const expr of node.expressions) visitExpression(expr, visitor);
|
|
3013
|
+
}
|
|
3014
|
+
function visitExprTypeAssertion(node, visitor) {
|
|
3015
|
+
if (visitor.visitExprTypeAssertion?.(node) === false) return;
|
|
3016
|
+
visitExpression(node.operand, visitor);
|
|
3017
|
+
}
|
|
3018
|
+
function visitExprIfElse(node, visitor) {
|
|
3019
|
+
if (visitor.visitExprIfElse?.(node) === false) return;
|
|
3020
|
+
visitExpression(node.condition, visitor);
|
|
3021
|
+
visitExpression(node.thenExpr, visitor);
|
|
3022
|
+
for (const elseif of node.elseifs) visitElseIfExpr(elseif, visitor);
|
|
3023
|
+
visitExpression(node.elseExpr, visitor);
|
|
3024
|
+
}
|
|
3025
|
+
function visitElseIfExpr(node, visitor) {
|
|
3026
|
+
visitExpression(node.condition, visitor);
|
|
3027
|
+
visitExpression(node.thenExpr, visitor);
|
|
3028
|
+
}
|
|
3029
|
+
function visitExprInstantiate(node, visitor) {
|
|
3030
|
+
if (visitor.visitExprInstantiate?.(node) === false) return;
|
|
3031
|
+
visitExpression(node.expr, visitor);
|
|
3032
|
+
}
|
|
3033
|
+
//#endregion
|
|
2675
3034
|
//#region src/formatters/github-actions.ts
|
|
2676
3035
|
const SEPARATOR = " · ";
|
|
2677
3036
|
function escapeData(value) {
|
|
@@ -3097,4 +3456,4 @@ function writeGameOutput(filePath, entries) {
|
|
|
3097
3456
|
fs$1.writeFileSync(absolutePath, JSON.stringify(entries, null, 2));
|
|
3098
3457
|
}
|
|
3099
3458
|
//#endregion
|
|
3100
|
-
export {
|
|
3459
|
+
export { formatBanner as A, DEFAULT_CONFIG as B, writeJsonFile as C, formatResult as D, formatMultiProjectResult as E, createStudioBackend as F, isValidBackend as G, VALID_BACKENDS as H, OpenCloudBackend as I, parseJestOutput as J, LuauScriptError as K, createOpenCloudBackend as L, loadConfig$1 as M, resolveConfig as N, formatTestSummary as O, StudioBackend as P, buildJestArgv as R, formatJson as S, formatFailure as T, defineConfig as U, ROOT_ONLY_KEYS as V, defineProject as W, resolveTsconfigDirectories as _, formatAnnotations as a, rojoProjectSchema as b, visitBlock as c, buildProjectJob as d, execute as f, processProjectResult as g, loadCoverageManifest as h, runTypecheck as i, combineSourceMappers as j, formatTypecheckSummary as k, visitExpression as l, formatExecuteOutput as m, parseGameOutput as n, formatJobSummary as o, executeBackend as p, extractJsonFromOutput as q, writeGameOutput as r, resolveGitHubActionsOptions as s, formatGameOutputNotice as t, visitStatement as u, collectPaths$1 as v, formatAgentMultiProject as w, findFormatterOptions as x, resolveNestedProjects as y, generateTestScript as z };
|