@isentinel/jest-roblox 0.2.6 → 0.3.0
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 +234 -46
- package/dist/cli.d.mts +2 -4
- package/dist/cli.mjs +113 -2812
- package/dist/index.d.mts +559 -40
- package/dist/index.mjs +2 -2
- package/dist/run-BEUPi80L.mjs +9834 -0
- package/dist/{executor-COuwZJJX.d.mts → schema-BpjBo-Aw.d.mts} +139 -301
- package/dist/sea-entry.cjs +40211 -40255
- package/package.json +29 -25
- package/plugin/JestRobloxRunner.rbxm +0 -0
- package/plugin/plugin.project.json +1 -1
- package/plugin/src/init.server.luau +39 -0
- package/plugin/src/test-in-run-mode.server.luau +117 -2
- package/dist/game-output-CCPIQMWm.mjs +0 -3643
- package/dist/sea/jest-roblox +0 -0
- package/plugin/out/shared/entry.luau +0 -9
- package/plugin/out/shared/instance-resolver.luau +0 -88
- package/plugin/out/shared/mock/CoreScriptSyncService.luau +0 -19
- package/plugin/out/shared/mock/FileSystemService.luau +0 -30
- package/plugin/out/shared/promise.luau +0 -2006
- package/plugin/out/shared/runner.luau +0 -301
- package/plugin/out/shared/setup-timing.luau +0 -89
- package/plugin/out/shared/snapshot-patch.luau +0 -94
|
@@ -1,3643 +0,0 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
import { type } from "arktype";
|
|
3
|
-
import assert from "node:assert";
|
|
4
|
-
import * as fs$1 from "node:fs";
|
|
5
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
-
import * as path$1 from "node:path";
|
|
7
|
-
import path, { dirname, join, relative } from "node:path";
|
|
8
|
-
import process from "node:process";
|
|
9
|
-
import color from "tinyrainbow";
|
|
10
|
-
import { WebSocketServer } from "ws";
|
|
11
|
-
import { homedir, tmpdir } from "node:os";
|
|
12
|
-
import * as crypto from "node:crypto";
|
|
13
|
-
import { randomUUID } from "node:crypto";
|
|
14
|
-
import buffer from "node:buffer";
|
|
15
|
-
import { createDefineConfig, loadConfig } from "c12";
|
|
16
|
-
import { defuFn } from "defu";
|
|
17
|
-
import { getTsconfig } from "get-tsconfig";
|
|
18
|
-
import { TraceMap, originalPositionFor, sourceContentFor } from "@jridgewell/trace-mapping";
|
|
19
|
-
import hljs from "highlight.js/lib/core";
|
|
20
|
-
import typescript from "highlight.js/lib/languages/typescript";
|
|
21
|
-
import { execFileSync } from "node:child_process";
|
|
22
|
-
import { parseJSONC } from "confbox";
|
|
23
|
-
import { Visitor, parseSync } from "oxc-parser";
|
|
24
|
-
//#region src/reporter/parser.ts
|
|
25
|
-
const TASK_SCRIPT_PREFIX = /^TaskScript:\d+:\s*/;
|
|
26
|
-
var LuauScriptError = class extends Error {
|
|
27
|
-
gameOutput;
|
|
28
|
-
constructor(rawMessage) {
|
|
29
|
-
super(rawMessage.replace(TASK_SCRIPT_PREFIX, ""));
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
const jestResultSchema = type({
|
|
33
|
-
numFailedTests: "number",
|
|
34
|
-
numPassedTests: "number",
|
|
35
|
-
numPendingTests: "number",
|
|
36
|
-
numTotalTests: "number",
|
|
37
|
-
startTime: "number",
|
|
38
|
-
success: "boolean",
|
|
39
|
-
testResults: "object[]"
|
|
40
|
-
});
|
|
41
|
-
function extractJsonFromOutput(output) {
|
|
42
|
-
const lines = output.split("\n");
|
|
43
|
-
let braceCount = 0;
|
|
44
|
-
let collecting = false;
|
|
45
|
-
const jsonLines = [];
|
|
46
|
-
for (const line of lines) {
|
|
47
|
-
if (!collecting && line.trim().startsWith("{")) {
|
|
48
|
-
collecting = true;
|
|
49
|
-
braceCount = 0;
|
|
50
|
-
jsonLines.length = 0;
|
|
51
|
-
}
|
|
52
|
-
if (!collecting) continue;
|
|
53
|
-
jsonLines.push(line);
|
|
54
|
-
braceCount += countBraces(line);
|
|
55
|
-
if (braceCount !== 0) continue;
|
|
56
|
-
const candidate = jsonLines.join("\n").trim();
|
|
57
|
-
if (isValidJson(candidate)) return candidate;
|
|
58
|
-
collecting = false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
function parseJestOutput(output) {
|
|
62
|
-
const trimmed = output.trim();
|
|
63
|
-
if (trimmed.startsWith("{")) try {
|
|
64
|
-
return parseParsedOutput(JSON.parse(trimmed));
|
|
65
|
-
} catch {}
|
|
66
|
-
const jsonString = extractJsonFromOutput(output);
|
|
67
|
-
if (jsonString === void 0) throw new Error(`No valid Jest result JSON found in output, output was:\n${output}`);
|
|
68
|
-
return parseParsedOutput(JSON.parse(jsonString));
|
|
69
|
-
}
|
|
70
|
-
function countBraces(line) {
|
|
71
|
-
let count = 0;
|
|
72
|
-
for (const character of line) {
|
|
73
|
-
if (character === "{") count++;
|
|
74
|
-
if (character === "}") count--;
|
|
75
|
-
}
|
|
76
|
-
return count;
|
|
77
|
-
}
|
|
78
|
-
function isValidJson(text) {
|
|
79
|
-
try {
|
|
80
|
-
JSON.parse(text);
|
|
81
|
-
return true;
|
|
82
|
-
} catch {
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
function extractExecutionError(object) {
|
|
87
|
-
let current = object;
|
|
88
|
-
while (current["parent"] !== void 0 && typeof current["parent"] === "object") current = current["parent"];
|
|
89
|
-
return typeof current["error"] === "string" ? current["error"] : "Unknown error";
|
|
90
|
-
}
|
|
91
|
-
function extractLuauTiming(parsed) {
|
|
92
|
-
const timing = parsed["_timing"];
|
|
93
|
-
if (timing === void 0 || timing === null || typeof timing !== "object") return;
|
|
94
|
-
const record = {};
|
|
95
|
-
for (const [key, value] of Object.entries(timing)) if (typeof value === "number") record[key] = value;
|
|
96
|
-
return Object.keys(record).length > 0 ? record : void 0;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Luau 1-based integer-keyed tables serialize as JSON arrays.
|
|
100
|
-
* Convert arrays to string-keyed Records with 1-based keys to match cov-map format.
|
|
101
|
-
*/
|
|
102
|
-
function normalizeHitCounts(data) {
|
|
103
|
-
if (Array.isArray(data)) {
|
|
104
|
-
const result = {};
|
|
105
|
-
let index = 0;
|
|
106
|
-
for (const element of data) {
|
|
107
|
-
result[String(index + 1)] = typeof element === "number" ? element : 0;
|
|
108
|
-
index++;
|
|
109
|
-
}
|
|
110
|
-
return result;
|
|
111
|
-
}
|
|
112
|
-
if (typeof data === "object" && data !== null) return data;
|
|
113
|
-
return {};
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Normalize branch hit counts from Luau's nested array format.
|
|
117
|
-
* Luau serializes `__cov_b` as an array of arrays: `[[0,0,0], [0,0]]`.
|
|
118
|
-
* Convert outer array to string-keyed Record with 1-based keys,
|
|
119
|
-
* keeping inner arrays as-is.
|
|
120
|
-
*/
|
|
121
|
-
function normalizeBranchCounts(data) {
|
|
122
|
-
if (Array.isArray(data)) {
|
|
123
|
-
const result = {};
|
|
124
|
-
let index = 0;
|
|
125
|
-
for (const inner of data) {
|
|
126
|
-
if (Array.isArray(inner)) result[String(index + 1)] = inner.map((value) => typeof value === "number" ? value : 0);
|
|
127
|
-
else result[String(index + 1)] = [];
|
|
128
|
-
index++;
|
|
129
|
-
}
|
|
130
|
-
return result;
|
|
131
|
-
}
|
|
132
|
-
if (typeof data === "object" && data !== null) return data;
|
|
133
|
-
return {};
|
|
134
|
-
}
|
|
135
|
-
function extractCoverageData(parsed) {
|
|
136
|
-
const coverage = parsed["_coverage"];
|
|
137
|
-
if (coverage === void 0 || coverage === null || typeof coverage !== "object") return;
|
|
138
|
-
const record = {};
|
|
139
|
-
for (const [key, value] of Object.entries(coverage)) if (typeof value === "object" && value !== null && "s" in value) {
|
|
140
|
-
const raw = value;
|
|
141
|
-
record[key] = {
|
|
142
|
-
b: raw.b !== void 0 ? normalizeBranchCounts(raw.b) : void 0,
|
|
143
|
-
f: raw.f !== void 0 ? normalizeHitCounts(raw.f) : void 0,
|
|
144
|
-
s: normalizeHitCounts(raw.s)
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
return Object.keys(record).length > 0 ? record : void 0;
|
|
148
|
-
}
|
|
149
|
-
function extractSnapshotWrites(parsed) {
|
|
150
|
-
const writes = parsed["_snapshotWrites"];
|
|
151
|
-
if (writes === void 0 || writes === null || typeof writes !== "object") return;
|
|
152
|
-
const record = {};
|
|
153
|
-
for (const [key, value] of Object.entries(writes)) if (typeof value === "string") record[key] = value;
|
|
154
|
-
return Object.keys(record).length > 0 ? record : void 0;
|
|
155
|
-
}
|
|
156
|
-
function stringifyError(err) {
|
|
157
|
-
if (typeof err === "string") return err;
|
|
158
|
-
if (typeof err === "object" && err !== null && "message" in err && typeof err.message === "string") return err.message;
|
|
159
|
-
if (typeof err === "object" && err !== null && "kind" in err && err["kind"] === "ExecutionError") return extractExecutionError(err);
|
|
160
|
-
const serialized = JSON.stringify(err);
|
|
161
|
-
assert(serialized !== void 0, "JSON-parsed values are always serializable");
|
|
162
|
-
return serialized;
|
|
163
|
-
}
|
|
164
|
-
function unwrapResult(parsed) {
|
|
165
|
-
if ("err" in parsed && parsed["success"] === false) throw new LuauScriptError(stringifyError(parsed["err"]));
|
|
166
|
-
if ("value" in parsed && parsed["success"] === true) return parsed["value"];
|
|
167
|
-
return parsed;
|
|
168
|
-
}
|
|
169
|
-
function validateJestResult(value) {
|
|
170
|
-
const result = jestResultSchema(value);
|
|
171
|
-
if (result instanceof type.errors) throw new Error(`Invalid Jest result: ${result.summary}`);
|
|
172
|
-
return result;
|
|
173
|
-
}
|
|
174
|
-
function extractSetupSeconds(parsed) {
|
|
175
|
-
const setup = parsed["_setup"];
|
|
176
|
-
if (typeof setup !== "number") return;
|
|
177
|
-
return setup;
|
|
178
|
-
}
|
|
179
|
-
function parseParsedOutput(parsed) {
|
|
180
|
-
const coverageData = extractCoverageData(parsed);
|
|
181
|
-
const luauTiming = extractLuauTiming(parsed);
|
|
182
|
-
const setupSeconds = extractSetupSeconds(parsed);
|
|
183
|
-
const snapshotWrites = extractSnapshotWrites(parsed);
|
|
184
|
-
const unwrapped = unwrapResult(parsed);
|
|
185
|
-
if (unwrapped["kind"] === "ExecutionError") {
|
|
186
|
-
const errorMessage = extractExecutionError(unwrapped);
|
|
187
|
-
throw new Error(`Jest execution failed: ${errorMessage}`);
|
|
188
|
-
}
|
|
189
|
-
if (unwrapped["results"] !== void 0 && typeof unwrapped["results"] === "object") return {
|
|
190
|
-
coverageData,
|
|
191
|
-
luauTiming,
|
|
192
|
-
result: validateJestResult(unwrapped["results"]),
|
|
193
|
-
setupSeconds,
|
|
194
|
-
snapshotWrites
|
|
195
|
-
};
|
|
196
|
-
return {
|
|
197
|
-
coverageData,
|
|
198
|
-
luauTiming,
|
|
199
|
-
result: validateJestResult(unwrapped),
|
|
200
|
-
setupSeconds,
|
|
201
|
-
snapshotWrites
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
//#endregion
|
|
205
|
-
//#region packages/roblox-runner/dist/index.mjs
|
|
206
|
-
const CACHE_DIR_NAME = "jest-roblox";
|
|
207
|
-
function getCacheDirectory() {
|
|
208
|
-
const xdgCacheHome = process.env["XDG_CACHE_HOME"];
|
|
209
|
-
if (xdgCacheHome !== void 0 && xdgCacheHome !== "") return path$1.join(xdgCacheHome, CACHE_DIR_NAME);
|
|
210
|
-
if (process.platform === "win32") {
|
|
211
|
-
const localAppData = process.env["LOCALAPPDATA"];
|
|
212
|
-
if (localAppData !== void 0 && localAppData !== "") return path$1.join(localAppData, CACHE_DIR_NAME);
|
|
213
|
-
return path$1.join(tmpdir(), CACHE_DIR_NAME);
|
|
214
|
-
}
|
|
215
|
-
return path$1.join(homedir(), ".cache", CACHE_DIR_NAME);
|
|
216
|
-
}
|
|
217
|
-
function getCacheKey(universeId, placeId) {
|
|
218
|
-
return `${universeId}:${placeId}`;
|
|
219
|
-
}
|
|
220
|
-
function isUploaded(cache, key, fileHash) {
|
|
221
|
-
return cache[key]?.fileHash === fileHash;
|
|
222
|
-
}
|
|
223
|
-
function markUploaded(cache, key, fileHash) {
|
|
224
|
-
cache[key] = {
|
|
225
|
-
fileHash,
|
|
226
|
-
uploadedAt: Date.now()
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
function readCache(cacheFilePath) {
|
|
230
|
-
try {
|
|
231
|
-
const data = fs$1.readFileSync(cacheFilePath, "utf-8");
|
|
232
|
-
return JSON.parse(data);
|
|
233
|
-
} catch {
|
|
234
|
-
return {};
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
function writeCache(cacheFilePath, cache) {
|
|
238
|
-
const cacheDirectory = path$1.dirname(cacheFilePath);
|
|
239
|
-
fs$1.mkdirSync(cacheDirectory, { recursive: true });
|
|
240
|
-
fs$1.writeFileSync(cacheFilePath, JSON.stringify(cache, null, 2));
|
|
241
|
-
}
|
|
242
|
-
function hashBuffer(data) {
|
|
243
|
-
return crypto.createHash("sha256").update(data).digest("hex");
|
|
244
|
-
}
|
|
245
|
-
function createFetchClient(defaultHeaders) {
|
|
246
|
-
return { async request(method, url, options) {
|
|
247
|
-
const headers = {
|
|
248
|
-
...defaultHeaders,
|
|
249
|
-
...options?.headers
|
|
250
|
-
};
|
|
251
|
-
const fetchOptions = {
|
|
252
|
-
headers,
|
|
253
|
-
method
|
|
254
|
-
};
|
|
255
|
-
if (options?.body !== void 0) if (options.body instanceof buffer.Buffer) fetchOptions.body = options.body;
|
|
256
|
-
else {
|
|
257
|
-
fetchOptions.body = JSON.stringify(options.body);
|
|
258
|
-
headers["Content-Type"] = "application/json";
|
|
259
|
-
}
|
|
260
|
-
const response = await fetch(url, fetchOptions);
|
|
261
|
-
return {
|
|
262
|
-
body: await (response.headers.get("content-type")?.includes("application/json") ?? false ? response.json() : response.text()),
|
|
263
|
-
headers: { "retry-after": response.headers.get("retry-after") ?? void 0 },
|
|
264
|
-
ok: response.ok,
|
|
265
|
-
status: response.status
|
|
266
|
-
};
|
|
267
|
-
} };
|
|
268
|
-
}
|
|
269
|
-
type({ path: "string" });
|
|
270
|
-
type({
|
|
271
|
-
"error?": { "message?": "string" },
|
|
272
|
-
"output?": { "results?": "string[]" },
|
|
273
|
-
"state": "'CANCELLED' | 'COMPLETE' | 'FAILED' | 'PROCESSING'"
|
|
274
|
-
});
|
|
275
|
-
type({
|
|
276
|
-
outputs: "string[]",
|
|
277
|
-
request_id: "string",
|
|
278
|
-
type: "'results'"
|
|
279
|
-
});
|
|
280
|
-
//#endregion
|
|
281
|
-
//#region src/config/schema.ts
|
|
282
|
-
const ROOT_ONLY_KEYS = new Set([
|
|
283
|
-
"backend",
|
|
284
|
-
"cache",
|
|
285
|
-
"collectCoverage",
|
|
286
|
-
"collectCoverageFrom",
|
|
287
|
-
"coverageDirectory",
|
|
288
|
-
"coveragePathIgnorePatterns",
|
|
289
|
-
"coverageReporters",
|
|
290
|
-
"coverageThreshold",
|
|
291
|
-
"formatters",
|
|
292
|
-
"gameOutput",
|
|
293
|
-
"jestPath",
|
|
294
|
-
"luauRoots",
|
|
295
|
-
"parallel",
|
|
296
|
-
"placeFile",
|
|
297
|
-
"pollInterval",
|
|
298
|
-
"port",
|
|
299
|
-
"rojoProject",
|
|
300
|
-
"rootDir",
|
|
301
|
-
"showLuau",
|
|
302
|
-
"sourceMap",
|
|
303
|
-
"timeout",
|
|
304
|
-
"typecheck",
|
|
305
|
-
"typecheckOnly",
|
|
306
|
-
"typecheckTsconfig"
|
|
307
|
-
]);
|
|
308
|
-
const VALID_BACKENDS = new Set([
|
|
309
|
-
"auto",
|
|
310
|
-
"open-cloud",
|
|
311
|
-
"studio"
|
|
312
|
-
]);
|
|
313
|
-
function isValidBackend(value) {
|
|
314
|
-
return VALID_BACKENDS.has(value);
|
|
315
|
-
}
|
|
316
|
-
const DEFAULT_CONFIG = {
|
|
317
|
-
backend: "auto",
|
|
318
|
-
cache: true,
|
|
319
|
-
collectCoverage: false,
|
|
320
|
-
color: true,
|
|
321
|
-
coverageDirectory: "coverage",
|
|
322
|
-
coveragePathIgnorePatterns: [
|
|
323
|
-
"**/*.spec.lua",
|
|
324
|
-
"**/*.spec.luau",
|
|
325
|
-
"**/*.test.lua",
|
|
326
|
-
"**/*.test.luau",
|
|
327
|
-
"**/node_modules/**",
|
|
328
|
-
"**/rbxts_include/**"
|
|
329
|
-
],
|
|
330
|
-
coverageReporters: ["text", "lcov"],
|
|
331
|
-
passWithNoTests: false,
|
|
332
|
-
placeFile: "./game.rbxl",
|
|
333
|
-
pollInterval: 500,
|
|
334
|
-
port: 3001,
|
|
335
|
-
rootDir: process.cwd(),
|
|
336
|
-
showLuau: true,
|
|
337
|
-
silent: false,
|
|
338
|
-
sourceMap: true,
|
|
339
|
-
testMatch: [
|
|
340
|
-
"**/*.spec.ts",
|
|
341
|
-
"**/*.spec.tsx",
|
|
342
|
-
"**/*.test.ts",
|
|
343
|
-
"**/*.test.tsx",
|
|
344
|
-
"**/*.spec-d.ts",
|
|
345
|
-
"**/*.test-d.ts",
|
|
346
|
-
"**/*.spec.lua",
|
|
347
|
-
"**/*.spec.luau",
|
|
348
|
-
"**/*.test.lua",
|
|
349
|
-
"**/*.test.luau"
|
|
350
|
-
],
|
|
351
|
-
testPathIgnorePatterns: [
|
|
352
|
-
"/node_modules/",
|
|
353
|
-
"/dist/",
|
|
354
|
-
"/out/"
|
|
355
|
-
],
|
|
356
|
-
timeout: 3e5,
|
|
357
|
-
typecheck: false,
|
|
358
|
-
typecheckOnly: false,
|
|
359
|
-
verbose: false
|
|
360
|
-
};
|
|
361
|
-
const snapshotFormatSchema = type({
|
|
362
|
-
"+": "reject",
|
|
363
|
-
"callToJSON?": "boolean",
|
|
364
|
-
"escapeRegex?": "boolean",
|
|
365
|
-
"escapeString?": "boolean",
|
|
366
|
-
"indent?": "number",
|
|
367
|
-
"maxDepth?": "number",
|
|
368
|
-
"min?": "boolean",
|
|
369
|
-
"printBasicPrototype?": "boolean",
|
|
370
|
-
"printFunctionName?": "boolean"
|
|
371
|
-
});
|
|
372
|
-
const coverageThresholdSchema = type({
|
|
373
|
-
"+": "reject",
|
|
374
|
-
"branches?": "number",
|
|
375
|
-
"functions?": "number",
|
|
376
|
-
"lines?": "number",
|
|
377
|
-
"statements?": "number"
|
|
378
|
-
});
|
|
379
|
-
const displayNameSchema = type({
|
|
380
|
-
"name": "string",
|
|
381
|
-
"+": "reject",
|
|
382
|
-
"color": "string"
|
|
383
|
-
});
|
|
384
|
-
const projectTestConfigSchema = type({
|
|
385
|
-
"+": "reject",
|
|
386
|
-
"automock?": "boolean",
|
|
387
|
-
"clearMocks?": "boolean",
|
|
388
|
-
"displayName": type("string").or(displayNameSchema),
|
|
389
|
-
"exclude?": "string[]",
|
|
390
|
-
"include": "string[]",
|
|
391
|
-
"injectGlobals?": "boolean",
|
|
392
|
-
"mockDataModel?": "boolean",
|
|
393
|
-
"outDir?": "string",
|
|
394
|
-
"resetMocks?": "boolean",
|
|
395
|
-
"resetModules?": "boolean",
|
|
396
|
-
"restoreMocks?": "boolean",
|
|
397
|
-
"root?": "string",
|
|
398
|
-
"setupFiles?": "string[]",
|
|
399
|
-
"setupFilesAfterEnv?": "string[]",
|
|
400
|
-
"slowTestThreshold?": "number",
|
|
401
|
-
"snapshotFormat?": snapshotFormatSchema,
|
|
402
|
-
"snapshotSerializers?": "string[]",
|
|
403
|
-
"testEnvironment?": "string",
|
|
404
|
-
"testEnvironmentOptions?": type("string").or(type("object")),
|
|
405
|
-
"testMatch?": "string[]",
|
|
406
|
-
"testPathIgnorePatterns?": "string[]",
|
|
407
|
-
"testRegex?": type("string").or(type("string[]")),
|
|
408
|
-
"testTimeout?": "number"
|
|
409
|
-
});
|
|
410
|
-
const inlineProjectSchema = type({
|
|
411
|
-
"+": "reject",
|
|
412
|
-
"test": projectTestConfigSchema
|
|
413
|
-
});
|
|
414
|
-
const formatterEntrySchema = type("string").or(type(["string", type("object")]));
|
|
415
|
-
const projectEntrySchema = type("string").or(inlineProjectSchema);
|
|
416
|
-
const configSchema = type({
|
|
417
|
-
"+": "reject",
|
|
418
|
-
"all?": "boolean",
|
|
419
|
-
"automock?": "boolean",
|
|
420
|
-
"backend?": type("'auto'|'open-cloud'|'studio'"),
|
|
421
|
-
"bail?": type("boolean").or(type("number")),
|
|
422
|
-
"cache?": "boolean",
|
|
423
|
-
"changedSince?": "string",
|
|
424
|
-
"ci?": "boolean",
|
|
425
|
-
"clearCache?": "boolean",
|
|
426
|
-
"clearMocks?": "boolean",
|
|
427
|
-
"collectCoverage?": "boolean",
|
|
428
|
-
"collectCoverageFrom?": "string[]",
|
|
429
|
-
"color?": "boolean",
|
|
430
|
-
"colors?": "boolean",
|
|
431
|
-
"config?": "string",
|
|
432
|
-
"coverage?": "boolean",
|
|
433
|
-
"coverageDirectory?": "string",
|
|
434
|
-
"coveragePathIgnorePatterns?": "string[]",
|
|
435
|
-
"coverageReporters?": "string[]",
|
|
436
|
-
"coverageThreshold?": coverageThresholdSchema,
|
|
437
|
-
"debug?": "boolean",
|
|
438
|
-
"env?": "string",
|
|
439
|
-
"expand?": "boolean",
|
|
440
|
-
"formatters?": formatterEntrySchema.array(),
|
|
441
|
-
"gameOutput?": "string",
|
|
442
|
-
"globals?": "string",
|
|
443
|
-
"init?": "boolean",
|
|
444
|
-
"injectGlobals?": "boolean",
|
|
445
|
-
"jestPath?": "string",
|
|
446
|
-
"luauRoots?": "string[]",
|
|
447
|
-
"maxWorkers?": type("number").or(type("string")),
|
|
448
|
-
"noStackTrace?": "boolean",
|
|
449
|
-
"outputFile?": "string",
|
|
450
|
-
"parallel?": type("'auto'").or("number.integer >= 1"),
|
|
451
|
-
"passWithNoTests?": "boolean",
|
|
452
|
-
"placeFile?": "string",
|
|
453
|
-
"pollInterval?": "number",
|
|
454
|
-
"port?": "number",
|
|
455
|
-
"preset?": "string",
|
|
456
|
-
"projects?": projectEntrySchema.array(),
|
|
457
|
-
"reporters?": "string[]",
|
|
458
|
-
"resetMocks?": "boolean",
|
|
459
|
-
"resetModules?": "boolean",
|
|
460
|
-
"restoreMocks?": "boolean",
|
|
461
|
-
"rojoProject?": "string",
|
|
462
|
-
"rootDir?": "string",
|
|
463
|
-
"roots?": "string[]",
|
|
464
|
-
"runInBand?": "boolean",
|
|
465
|
-
"selectProjects?": "string[]",
|
|
466
|
-
"setupFiles?": "string[]",
|
|
467
|
-
"setupFilesAfterEnv?": "string[]",
|
|
468
|
-
"showConfig?": "boolean",
|
|
469
|
-
"showLuau?": "boolean",
|
|
470
|
-
"silent?": "boolean",
|
|
471
|
-
"snapshotFormat?": snapshotFormatSchema,
|
|
472
|
-
"snapshotSerializers?": "string[]",
|
|
473
|
-
"sourceMap?": "boolean",
|
|
474
|
-
"testEnvironment?": "string",
|
|
475
|
-
"testEnvironmentOptions?": type("string").or(type("object")),
|
|
476
|
-
"testFailureExitCode?": "string",
|
|
477
|
-
"testMatch?": "string[]",
|
|
478
|
-
"testNamePattern?": "string",
|
|
479
|
-
"testPathIgnorePatterns?": "string[]",
|
|
480
|
-
"testPathPattern?": "string",
|
|
481
|
-
"testRegex?": type("string").or(type("string[]")),
|
|
482
|
-
"testTimeout?": "number",
|
|
483
|
-
"timeout?": "number",
|
|
484
|
-
"timers?": "string",
|
|
485
|
-
"typecheck?": "boolean",
|
|
486
|
-
"typecheckOnly?": "boolean",
|
|
487
|
-
"typecheckTsconfig?": "string",
|
|
488
|
-
"updateSnapshot?": "boolean",
|
|
489
|
-
"verbose?": "boolean",
|
|
490
|
-
"version?": "boolean"
|
|
491
|
-
}).as();
|
|
492
|
-
function validateConfig(raw) {
|
|
493
|
-
const result = configSchema(raw);
|
|
494
|
-
if (result instanceof type.errors) throw new Error(`Invalid config: ${result.summary}`);
|
|
495
|
-
return result;
|
|
496
|
-
}
|
|
497
|
-
const defineConfig = createDefineConfig();
|
|
498
|
-
const defineProject = createDefineConfig();
|
|
499
|
-
//#endregion
|
|
500
|
-
//#region src/test-runner.bundled.luau
|
|
501
|
-
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";
|
|
502
|
-
//#endregion
|
|
503
|
-
//#region src/test-script.ts
|
|
504
|
-
function buildJestArgv(options) {
|
|
505
|
-
const argv = {};
|
|
506
|
-
for (const [key, value] of Object.entries(options.config)) if (!ROOT_ONLY_KEYS.has(key) && value !== void 0) argv[key] = value;
|
|
507
|
-
if (options.config.jestPath !== void 0) argv["jestPath"] = options.config.jestPath;
|
|
508
|
-
if (process.env["TIMING"] !== void 0) argv["_timing"] = true;
|
|
509
|
-
if (options.config.collectCoverage) argv["_coverage"] = true;
|
|
510
|
-
return {
|
|
511
|
-
...argv,
|
|
512
|
-
reporters: argv["reporters"] ?? [],
|
|
513
|
-
testMatch: options.config.testMatch.map((pattern) => pattern.replace(/\.(tsx?|luau?)$/, ""))
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
function generateTestScript(options) {
|
|
517
|
-
const configs = (Array.isArray(options) ? options : [options]).map((input) => buildJestArgv(input));
|
|
518
|
-
return test_runner_bundled_default.replace("__CONFIG_JSON__", () => JSON.stringify({ configs }));
|
|
519
|
-
}
|
|
520
|
-
//#endregion
|
|
521
|
-
//#region src/backends/open-cloud.ts
|
|
522
|
-
const PARALLEL_AUTO_CAP = 3;
|
|
523
|
-
const DEFAULT_OPEN_CLOUD_BASE_URL = "https://apis.roblox.com";
|
|
524
|
-
const RATE_LIMIT_DEFAULT_WAIT_MS = 5e3;
|
|
525
|
-
const MAX_RATE_LIMIT_RETRIES = 5;
|
|
526
|
-
const taskResponse = type({ path: "string" });
|
|
527
|
-
const taskStatusResponse = type({
|
|
528
|
-
"error?": { "message?": "string" },
|
|
529
|
-
"output?": { "results?": "string[]" },
|
|
530
|
-
"state": "'CANCELLED' | 'COMPLETE' | 'FAILED' | 'PROCESSING'"
|
|
531
|
-
});
|
|
532
|
-
const envelopeSchema$1 = type({ entries: type({
|
|
533
|
-
"elapsedMs?": "number",
|
|
534
|
-
"gameOutput?": "string",
|
|
535
|
-
"jestOutput": "string"
|
|
536
|
-
}).array() });
|
|
537
|
-
var OpenCloudBackend = class {
|
|
538
|
-
baseUrl;
|
|
539
|
-
credentials;
|
|
540
|
-
http;
|
|
541
|
-
readFile;
|
|
542
|
-
sleepFn;
|
|
543
|
-
kind = "open-cloud";
|
|
544
|
-
constructor(credentials, options) {
|
|
545
|
-
this.baseUrl = resolveOpenCloudBaseUrl();
|
|
546
|
-
this.credentials = credentials;
|
|
547
|
-
this.http = options?.http ?? createFetchClient({ "x-api-key": credentials.apiKey });
|
|
548
|
-
this.readFile = options?.readFile ?? ((filePath) => fs$1.readFileSync(filePath));
|
|
549
|
-
this.sleepFn = options?.sleep ?? (async (ms) => {
|
|
550
|
-
return new Promise((resolve) => {
|
|
551
|
-
setTimeout(resolve, ms);
|
|
552
|
-
});
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
async runTests(options) {
|
|
556
|
-
const { jobs, parallel } = options;
|
|
557
|
-
if (jobs.length === 0) throw new Error("OpenCloudBackend requires at least one job");
|
|
558
|
-
const primary = jobs[0];
|
|
559
|
-
const placeFilePath = path$1.resolve(primary.config.rootDir, primary.config.placeFile);
|
|
560
|
-
const cacheDirectory = getCacheDirectory();
|
|
561
|
-
const cacheFilePath = path$1.join(cacheDirectory, "upload-cache.json");
|
|
562
|
-
const uploadStart = Date.now();
|
|
563
|
-
const placeData = this.readFile(placeFilePath);
|
|
564
|
-
const fileHash = hashBuffer(placeData);
|
|
565
|
-
const cacheKey = getCacheKey(this.credentials.universeId, this.credentials.placeId);
|
|
566
|
-
const cache = readCache(cacheFilePath);
|
|
567
|
-
const uploadCached = await this.uploadOrReuseCached({
|
|
568
|
-
cache,
|
|
569
|
-
cacheEnabled: primary.config.cache,
|
|
570
|
-
cacheFilePath,
|
|
571
|
-
cacheKey,
|
|
572
|
-
fileHash,
|
|
573
|
-
placeData
|
|
574
|
-
});
|
|
575
|
-
const uploadMs = Date.now() - uploadStart;
|
|
576
|
-
const buckets = bucketJobs(jobs, parallel);
|
|
577
|
-
const executionStart = Date.now();
|
|
578
|
-
const bucketResults = await Promise.all(buckets.map(async (bucket) => this.runBucket(bucket)));
|
|
579
|
-
const executionMs = Date.now() - executionStart;
|
|
580
|
-
const flattened = Array.from({ length: jobs.length });
|
|
581
|
-
for (const { indices, results } of bucketResults) for (const [positionInBucket, originalIndex] of indices.entries()) flattened[originalIndex] = results[positionInBucket];
|
|
582
|
-
return {
|
|
583
|
-
results: flattened,
|
|
584
|
-
timing: {
|
|
585
|
-
executionMs,
|
|
586
|
-
uploadCached,
|
|
587
|
-
uploadMs
|
|
588
|
-
}
|
|
589
|
-
};
|
|
590
|
-
}
|
|
591
|
-
async createExecutionTask(inputs, timeoutMs) {
|
|
592
|
-
const url = `${this.baseUrl}/cloud/v2/universes/${this.credentials.universeId}/places/${this.credentials.placeId}/luau-execution-session-tasks`;
|
|
593
|
-
const script = generateTestScript(inputs);
|
|
594
|
-
const response = await this.http.request("POST", url, { body: {
|
|
595
|
-
script,
|
|
596
|
-
timeout: `${Math.floor(timeoutMs / 1e3)}s`
|
|
597
|
-
} });
|
|
598
|
-
if (!response.ok) throw new Error(`Failed to create execution task: ${response.status}`);
|
|
599
|
-
return taskResponse.assert(response.body).path;
|
|
600
|
-
}
|
|
601
|
-
async pollForCompletion(taskPath, timeoutMs, pollIntervalMs) {
|
|
602
|
-
const url = `${this.baseUrl}/cloud/v2/${taskPath}`;
|
|
603
|
-
const startTime = Date.now();
|
|
604
|
-
let rateLimitRetries = 0;
|
|
605
|
-
while (Date.now() - startTime < timeoutMs) {
|
|
606
|
-
const response = await this.http.request("GET", url);
|
|
607
|
-
if (response.status === 429) {
|
|
608
|
-
rateLimitRetries++;
|
|
609
|
-
if (rateLimitRetries > MAX_RATE_LIMIT_RETRIES) throw new Error("Rate limited by Open Cloud API after multiple retries");
|
|
610
|
-
const retryAfter = parseRetryAfter(response.headers);
|
|
611
|
-
await this.sleepFn(retryAfter);
|
|
612
|
-
continue;
|
|
613
|
-
}
|
|
614
|
-
if (!response.ok) throw new Error(`Failed to poll task: ${response.status}`);
|
|
615
|
-
const body = taskStatusResponse.assert(response.body);
|
|
616
|
-
switch (body.state) {
|
|
617
|
-
case "COMPLETE": {
|
|
618
|
-
const value = body.output?.results?.[0];
|
|
619
|
-
if (value === void 0) throw new Error(`No test results in output. Got: ${JSON.stringify(body.output)}`);
|
|
620
|
-
return {
|
|
621
|
-
gameOutput: body.output?.results?.[1],
|
|
622
|
-
jestOutput: value
|
|
623
|
-
};
|
|
624
|
-
}
|
|
625
|
-
case "FAILED": throw new Error(body.error?.message ?? "Execution failed");
|
|
626
|
-
case "CANCELLED": throw new Error("Execution was cancelled");
|
|
627
|
-
case "PROCESSING":
|
|
628
|
-
await this.sleepFn(pollIntervalMs);
|
|
629
|
-
break;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
throw new Error("Execution timed out");
|
|
633
|
-
}
|
|
634
|
-
async runBucket(bucket) {
|
|
635
|
-
const { indices, jobs } = bucket;
|
|
636
|
-
const primary = jobs[0];
|
|
637
|
-
const inputs = jobs.map((job) => {
|
|
638
|
-
return {
|
|
639
|
-
config: job.config,
|
|
640
|
-
testFiles: job.testFiles
|
|
641
|
-
};
|
|
642
|
-
});
|
|
643
|
-
const taskPath = await this.createExecutionTask(inputs, primary.config.timeout);
|
|
644
|
-
const { gameOutput, jestOutput } = await this.pollForCompletion(taskPath, primary.config.timeout, primary.config.pollInterval);
|
|
645
|
-
const entries = parseEnvelope(jestOutput);
|
|
646
|
-
if (entries.length !== jobs.length) throw new Error(`Open Cloud backend returned ${entries.length.toString()} entries but bucket had ${jobs.length.toString()} jobs`);
|
|
647
|
-
return {
|
|
648
|
-
indices,
|
|
649
|
-
results: entries.map((entry, index) => {
|
|
650
|
-
return buildProjectResult(entry, jobs[index], gameOutput);
|
|
651
|
-
})
|
|
652
|
-
};
|
|
653
|
-
}
|
|
654
|
-
async uploadOrReuseCached({ cache, cacheEnabled, cacheFilePath, cacheKey, fileHash, placeData }) {
|
|
655
|
-
if (cacheEnabled && isUploaded(cache, cacheKey, fileHash)) return true;
|
|
656
|
-
await this.uploadPlaceData(placeData);
|
|
657
|
-
markUploaded(cache, cacheKey, fileHash);
|
|
658
|
-
writeCache(cacheFilePath, cache);
|
|
659
|
-
return false;
|
|
660
|
-
}
|
|
661
|
-
async uploadPlaceData(placeData) {
|
|
662
|
-
const url = `${this.baseUrl}/universes/v1/${this.credentials.universeId}/places/${this.credentials.placeId}/versions?versionType=Saved`;
|
|
663
|
-
const response = await this.http.request("POST", url, {
|
|
664
|
-
body: placeData,
|
|
665
|
-
headers: { "Content-Type": "application/octet-stream" }
|
|
666
|
-
});
|
|
667
|
-
if (!response.ok) throw new Error(`Failed to upload place: ${response.status}`);
|
|
668
|
-
}
|
|
669
|
-
};
|
|
670
|
-
function createOpenCloudBackend() {
|
|
671
|
-
const apiKey = process.env["ROBLOX_OPEN_CLOUD_API_KEY"];
|
|
672
|
-
if (apiKey === void 0) throw new Error("ROBLOX_OPEN_CLOUD_API_KEY environment variable is required");
|
|
673
|
-
const universeId = process.env["ROBLOX_UNIVERSE_ID"];
|
|
674
|
-
if (universeId === void 0) throw new Error("ROBLOX_UNIVERSE_ID environment variable is required");
|
|
675
|
-
const placeId = process.env["ROBLOX_PLACE_ID"];
|
|
676
|
-
if (placeId === void 0) throw new Error("ROBLOX_PLACE_ID environment variable is required");
|
|
677
|
-
return new OpenCloudBackend({
|
|
678
|
-
apiKey,
|
|
679
|
-
placeId,
|
|
680
|
-
universeId
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
function resolveOpenCloudBaseUrl() {
|
|
684
|
-
const override = process.env["JEST_ROBLOX_OPEN_CLOUD_BASE_URL"];
|
|
685
|
-
if (override === void 0 || override.trim() === "") return DEFAULT_OPEN_CLOUD_BASE_URL;
|
|
686
|
-
return override.replace(/\/+$/, "");
|
|
687
|
-
}
|
|
688
|
-
function resolveBucketCount(parallel, jobCount) {
|
|
689
|
-
if (parallel === void 0) return 1;
|
|
690
|
-
if (parallel === "auto") return Math.min(jobCount, PARALLEL_AUTO_CAP);
|
|
691
|
-
if (parallel < 1) throw new Error(`--parallel must be >= 1, got ${parallel.toString()}`);
|
|
692
|
-
return Math.min(Math.floor(parallel), jobCount);
|
|
693
|
-
}
|
|
694
|
-
function bucketJobs(jobs, parallel) {
|
|
695
|
-
const bucketCount = resolveBucketCount(parallel, jobs.length);
|
|
696
|
-
const buckets = [];
|
|
697
|
-
for (let index = 0; index < bucketCount; index++) buckets.push({
|
|
698
|
-
indices: [],
|
|
699
|
-
jobs: []
|
|
700
|
-
});
|
|
701
|
-
for (const [originalIndex, job] of jobs.entries()) {
|
|
702
|
-
const bucket = buckets[originalIndex % bucketCount];
|
|
703
|
-
bucket.indices.push(originalIndex);
|
|
704
|
-
bucket.jobs.push(job);
|
|
705
|
-
}
|
|
706
|
-
return buckets;
|
|
707
|
-
}
|
|
708
|
-
function parseEnvelope(jestOutput) {
|
|
709
|
-
const envelope = envelopeSchema$1(JSON.parse(jestOutput));
|
|
710
|
-
if (envelope instanceof type.errors) return [{
|
|
711
|
-
elapsedMs: 0,
|
|
712
|
-
jestOutput
|
|
713
|
-
}];
|
|
714
|
-
return envelope.entries;
|
|
715
|
-
}
|
|
716
|
-
function buildProjectResult(entry, job, fallbackGameOutput) {
|
|
717
|
-
const gameOutput = entry.gameOutput ?? fallbackGameOutput;
|
|
718
|
-
let parsed;
|
|
719
|
-
try {
|
|
720
|
-
parsed = parseJestOutput(entry.jestOutput);
|
|
721
|
-
} catch (err) {
|
|
722
|
-
if (err instanceof LuauScriptError) err.gameOutput = gameOutput;
|
|
723
|
-
throw err;
|
|
724
|
-
}
|
|
725
|
-
return {
|
|
726
|
-
coverageData: parsed.coverageData,
|
|
727
|
-
displayColor: job.displayColor,
|
|
728
|
-
displayName: job.displayName,
|
|
729
|
-
elapsedMs: entry.elapsedMs ?? 0,
|
|
730
|
-
gameOutput,
|
|
731
|
-
luauTiming: parsed.luauTiming,
|
|
732
|
-
result: parsed.result,
|
|
733
|
-
setupMs: parsed.setupSeconds !== void 0 ? Math.round(parsed.setupSeconds * 1e3) : void 0,
|
|
734
|
-
snapshotWrites: parsed.snapshotWrites
|
|
735
|
-
};
|
|
736
|
-
}
|
|
737
|
-
function parseRetryAfter(headers) {
|
|
738
|
-
const value = headers?.["retry-after"];
|
|
739
|
-
if (value === void 0) return RATE_LIMIT_DEFAULT_WAIT_MS;
|
|
740
|
-
const seconds = Number(value);
|
|
741
|
-
if (Number.isNaN(seconds) || seconds <= 0) return RATE_LIMIT_DEFAULT_WAIT_MS;
|
|
742
|
-
return seconds * 1e3;
|
|
743
|
-
}
|
|
744
|
-
//#endregion
|
|
745
|
-
//#region src/backends/studio.ts
|
|
746
|
-
const DEFAULT_STUDIO_TIMEOUT = 3e5;
|
|
747
|
-
const envelopeSchema = type({ entries: type({
|
|
748
|
-
"elapsedMs?": "number",
|
|
749
|
-
"gameOutput?": "string",
|
|
750
|
-
"jestOutput": "string"
|
|
751
|
-
}).array() });
|
|
752
|
-
const pluginMessageSchema = type({
|
|
753
|
-
"gameOutput?": "string",
|
|
754
|
-
"jestOutput": "string",
|
|
755
|
-
"request_id": "string",
|
|
756
|
-
"type": "'results'"
|
|
757
|
-
});
|
|
758
|
-
var StudioBackend = class {
|
|
759
|
-
createServer;
|
|
760
|
-
port;
|
|
761
|
-
timeout;
|
|
762
|
-
preConnected;
|
|
763
|
-
wss;
|
|
764
|
-
kind = "studio";
|
|
765
|
-
constructor(options) {
|
|
766
|
-
this.port = options.port;
|
|
767
|
-
this.timeout = options.timeout ?? DEFAULT_STUDIO_TIMEOUT;
|
|
768
|
-
this.createServer = options.createServer ?? ((port) => new WebSocketServer({ port }));
|
|
769
|
-
this.preConnected = options.preConnected;
|
|
770
|
-
}
|
|
771
|
-
close() {
|
|
772
|
-
const server = this.wss;
|
|
773
|
-
this.wss = void 0;
|
|
774
|
-
if (server === void 0) return;
|
|
775
|
-
server.close();
|
|
776
|
-
}
|
|
777
|
-
async runTests(options) {
|
|
778
|
-
const pre = this.preConnected;
|
|
779
|
-
this.preConnected = void 0;
|
|
780
|
-
this.wss ??= pre?.server ?? this.createServer(this.port);
|
|
781
|
-
return this.executeViaPlugin(this.wss, options.jobs, pre?.socket);
|
|
782
|
-
}
|
|
783
|
-
buildProjectResult(entry, job, fallbackGameOutput) {
|
|
784
|
-
const gameOutput = entry.gameOutput ?? fallbackGameOutput;
|
|
785
|
-
let parsed;
|
|
786
|
-
try {
|
|
787
|
-
parsed = parseJestOutput(entry.jestOutput);
|
|
788
|
-
} catch (err) {
|
|
789
|
-
if (err instanceof LuauScriptError) err.gameOutput = gameOutput;
|
|
790
|
-
throw err;
|
|
791
|
-
}
|
|
792
|
-
return {
|
|
793
|
-
coverageData: parsed.coverageData,
|
|
794
|
-
displayColor: job.displayColor,
|
|
795
|
-
displayName: job.displayName,
|
|
796
|
-
elapsedMs: entry.elapsedMs ?? 0,
|
|
797
|
-
gameOutput,
|
|
798
|
-
luauTiming: parsed.luauTiming,
|
|
799
|
-
result: parsed.result,
|
|
800
|
-
setupMs: parsed.setupSeconds !== void 0 ? Math.round(parsed.setupSeconds * 1e3) : void 0,
|
|
801
|
-
snapshotWrites: parsed.snapshotWrites
|
|
802
|
-
};
|
|
803
|
-
}
|
|
804
|
-
async executeViaPlugin(wss, jobs, existingSocket) {
|
|
805
|
-
const requestId = randomUUID();
|
|
806
|
-
const configs = jobs.map((job) => buildJestArgv(job));
|
|
807
|
-
const executionStart = Date.now();
|
|
808
|
-
const message = await this.waitForResult(wss, requestId, configs, existingSocket);
|
|
809
|
-
const executionMs = Date.now() - executionStart;
|
|
810
|
-
const entries = this.parseEnvelope(message.jestOutput);
|
|
811
|
-
if (entries.length !== jobs.length) throw new Error(`Studio backend returned ${entries.length.toString()} entries but request had ${jobs.length.toString()} jobs`);
|
|
812
|
-
return {
|
|
813
|
-
results: entries.map((entry, index) => {
|
|
814
|
-
const matched = jobs[index];
|
|
815
|
-
return this.buildProjectResult(entry, matched, message.gameOutput);
|
|
816
|
-
}),
|
|
817
|
-
timing: { executionMs }
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
|
-
parseEnvelope(jestOutput) {
|
|
821
|
-
const envelope = envelopeSchema(JSON.parse(jestOutput));
|
|
822
|
-
if (envelope instanceof type.errors) return [{
|
|
823
|
-
elapsedMs: 0,
|
|
824
|
-
jestOutput
|
|
825
|
-
}];
|
|
826
|
-
return envelope.entries;
|
|
827
|
-
}
|
|
828
|
-
async waitForResult(wss, requestId, configs, existingSocket) {
|
|
829
|
-
return new Promise((resolve, reject) => {
|
|
830
|
-
const timer = setTimeout(() => {
|
|
831
|
-
reject(/* @__PURE__ */ new Error("Timed out waiting for Studio plugin connection"));
|
|
832
|
-
}, this.timeout);
|
|
833
|
-
function attachSocket(ws) {
|
|
834
|
-
ws.send(JSON.stringify({
|
|
835
|
-
action: "run_tests",
|
|
836
|
-
config: { configs },
|
|
837
|
-
request_id: requestId
|
|
838
|
-
}));
|
|
839
|
-
ws.on("message", (data) => {
|
|
840
|
-
const message = pluginMessageSchema(JSON.parse(data.toString()));
|
|
841
|
-
if (message instanceof type.errors) {
|
|
842
|
-
clearTimeout(timer);
|
|
843
|
-
reject(/* @__PURE__ */ new Error(`Invalid plugin message: ${message.summary}`));
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
|
-
if (message.request_id === requestId) {
|
|
847
|
-
clearTimeout(timer);
|
|
848
|
-
resolve(message);
|
|
849
|
-
}
|
|
850
|
-
});
|
|
851
|
-
ws.on("close", () => {
|
|
852
|
-
clearTimeout(timer);
|
|
853
|
-
reject(/* @__PURE__ */ new Error("Studio plugin disconnected before sending results"));
|
|
854
|
-
});
|
|
855
|
-
ws.on("error", (err) => {
|
|
856
|
-
clearTimeout(timer);
|
|
857
|
-
reject(err);
|
|
858
|
-
});
|
|
859
|
-
}
|
|
860
|
-
if (existingSocket) attachSocket(existingSocket);
|
|
861
|
-
wss.on("connection", (ws) => {
|
|
862
|
-
attachSocket(ws);
|
|
863
|
-
});
|
|
864
|
-
wss.on("error", (err) => {
|
|
865
|
-
clearTimeout(timer);
|
|
866
|
-
reject(err);
|
|
867
|
-
});
|
|
868
|
-
});
|
|
869
|
-
}
|
|
870
|
-
};
|
|
871
|
-
function createStudioBackend(options) {
|
|
872
|
-
return new StudioBackend(options);
|
|
873
|
-
}
|
|
874
|
-
//#endregion
|
|
875
|
-
//#region src/config/loader.ts
|
|
876
|
-
function applySnapshotFormatDefaults(config, isLuauProject) {
|
|
877
|
-
if (config.snapshotFormat?.printBasicPrototype !== void 0) return config;
|
|
878
|
-
return {
|
|
879
|
-
...config,
|
|
880
|
-
snapshotFormat: {
|
|
881
|
-
...config.snapshotFormat,
|
|
882
|
-
printBasicPrototype: isLuauProject
|
|
883
|
-
}
|
|
884
|
-
};
|
|
885
|
-
}
|
|
886
|
-
function resolveConfig(config) {
|
|
887
|
-
validateConfig(config);
|
|
888
|
-
const defined = Object.fromEntries(Object.entries(config).filter(([, value]) => value !== void 0));
|
|
889
|
-
return Object.assign({}, DEFAULT_CONFIG, defined);
|
|
890
|
-
}
|
|
891
|
-
async function loadConfig$1(configPath, cwd = process.cwd()) {
|
|
892
|
-
let result;
|
|
893
|
-
const extendWarnings = [];
|
|
894
|
-
const originalWarn = console.warn;
|
|
895
|
-
try {
|
|
896
|
-
console.warn = (...args) => {
|
|
897
|
-
const message = args.join(" ");
|
|
898
|
-
if (typeof message === "string" && message.includes("Cannot extend config")) {
|
|
899
|
-
extendWarnings.push(message);
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
|
-
originalWarn.apply(console, args);
|
|
903
|
-
};
|
|
904
|
-
result = await loadConfig({
|
|
905
|
-
name: "jest",
|
|
906
|
-
configFile: configPath,
|
|
907
|
-
configFileRequired: configPath !== void 0,
|
|
908
|
-
cwd,
|
|
909
|
-
dotenv: false,
|
|
910
|
-
globalRc: false,
|
|
911
|
-
import: isSea() ? seaImport : void 0,
|
|
912
|
-
merger,
|
|
913
|
-
omit$Keys: true,
|
|
914
|
-
packageJson: false,
|
|
915
|
-
rcFile: false
|
|
916
|
-
});
|
|
917
|
-
} catch (err) {
|
|
918
|
-
if (configPath !== void 0) throw new Error(`Config file not found: ${configPath}`, { cause: err });
|
|
919
|
-
throw err;
|
|
920
|
-
} finally {
|
|
921
|
-
console.warn = originalWarn;
|
|
922
|
-
}
|
|
923
|
-
if (extendWarnings.length > 0) {
|
|
924
|
-
const extendsPath = extendWarnings[0]?.match(/`([^`]+)`/)?.[1];
|
|
925
|
-
throw new Error(`Failed to resolve extends: "${extendsPath}". If the file exists, try adding the file extension (e.g. ".ts").`);
|
|
926
|
-
}
|
|
927
|
-
const config = resolveFunctionValues(result.config);
|
|
928
|
-
config.rootDir ??= cwd;
|
|
929
|
-
return resolveConfig(config);
|
|
930
|
-
}
|
|
931
|
-
function isSea() {
|
|
932
|
-
return process.env["JEST_ROBLOX_SEA"] === "true";
|
|
933
|
-
}
|
|
934
|
-
async function seaImport(id) {
|
|
935
|
-
if (id.endsWith(".json")) {
|
|
936
|
-
const content = readFileSync(id, "utf-8");
|
|
937
|
-
return JSON.parse(content);
|
|
938
|
-
}
|
|
939
|
-
return import(id);
|
|
940
|
-
}
|
|
941
|
-
function merger(...sources) {
|
|
942
|
-
return defuFn(...sources.filter(Boolean));
|
|
943
|
-
}
|
|
944
|
-
function isMergerFunction(value) {
|
|
945
|
-
return typeof value === "function";
|
|
946
|
-
}
|
|
947
|
-
function resolveFunctionValues(config) {
|
|
948
|
-
const resolved = {};
|
|
949
|
-
for (const [key, value] of Object.entries(config)) resolved[key] = isMergerFunction(value) ? value(void 0) : value;
|
|
950
|
-
return resolved;
|
|
951
|
-
}
|
|
952
|
-
//#endregion
|
|
953
|
-
//#region packages/rojo-utils/dist/index.mjs
|
|
954
|
-
function resolveNestedProjects(tree, rootDirectory) {
|
|
955
|
-
return resolveTree(tree, rootDirectory, rootDirectory, /* @__PURE__ */ new Set());
|
|
956
|
-
}
|
|
957
|
-
function collectPaths(node, result) {
|
|
958
|
-
for (const [key, value] of Object.entries(node)) if (key === "$path" && typeof value === "string") result.push(value.replaceAll("\\", "/"));
|
|
959
|
-
else if (typeof value === "object" && !Array.isArray(value) && !key.startsWith("$")) collectPaths(value, result);
|
|
960
|
-
}
|
|
961
|
-
function inlineNestedProject(projectPath, currentDirectory, originalRoot, visited) {
|
|
962
|
-
const chain = new Set(visited);
|
|
963
|
-
chain.add(projectPath);
|
|
964
|
-
let content;
|
|
965
|
-
try {
|
|
966
|
-
content = readFileSync(projectPath, "utf-8");
|
|
967
|
-
} catch (err) {
|
|
968
|
-
const relativePath = relative(currentDirectory, projectPath);
|
|
969
|
-
throw new Error(`Could not read nested Rojo project: ${relativePath}`, { cause: err });
|
|
970
|
-
}
|
|
971
|
-
let project;
|
|
972
|
-
try {
|
|
973
|
-
project = JSON.parse(content);
|
|
974
|
-
} catch (err) {
|
|
975
|
-
const relativePath = relative(currentDirectory, projectPath);
|
|
976
|
-
throw new Error(`Failed to parse nested Rojo project: ${relativePath}`, { cause: err });
|
|
977
|
-
}
|
|
978
|
-
return resolveTree(project.tree, dirname(projectPath), originalRoot, chain);
|
|
979
|
-
}
|
|
980
|
-
function resolveRootRelativePath(currentDirectory, value, originalRoot) {
|
|
981
|
-
return relative(originalRoot, join(currentDirectory, value)).replaceAll("\\", "/");
|
|
982
|
-
}
|
|
983
|
-
function resolveTree(node, currentDirectory, originalRoot, visited) {
|
|
984
|
-
const resolved = {};
|
|
985
|
-
for (const [key, value] of Object.entries(node)) {
|
|
986
|
-
if (key === "$path" && typeof value === "string" && value.endsWith(".project.json")) {
|
|
987
|
-
const projectPath = join(currentDirectory, value);
|
|
988
|
-
if (visited.has(projectPath)) throw new Error(`Circular project reference: ${value}`);
|
|
989
|
-
const innerTree = inlineNestedProject(projectPath, currentDirectory, originalRoot, visited);
|
|
990
|
-
for (const [innerKey, innerValue] of Object.entries(innerTree)) resolved[innerKey] = innerValue;
|
|
991
|
-
continue;
|
|
992
|
-
}
|
|
993
|
-
if (key === "$path" && typeof value === "string") {
|
|
994
|
-
resolved[key] = resolveRootRelativePath(currentDirectory, value, originalRoot);
|
|
995
|
-
continue;
|
|
996
|
-
}
|
|
997
|
-
if (key.startsWith("$") || typeof value !== "object" || Array.isArray(value)) {
|
|
998
|
-
resolved[key] = value;
|
|
999
|
-
continue;
|
|
1000
|
-
}
|
|
1001
|
-
resolved[key] = resolveTree(value, currentDirectory, originalRoot, visited);
|
|
1002
|
-
}
|
|
1003
|
-
return resolved;
|
|
1004
|
-
}
|
|
1005
|
-
function collectMounts(node, currentDataModelPath, classify) {
|
|
1006
|
-
const result = [];
|
|
1007
|
-
walk(node, currentDataModelPath, classify, result);
|
|
1008
|
-
return result;
|
|
1009
|
-
}
|
|
1010
|
-
function pruneAncestors(paths) {
|
|
1011
|
-
return paths.filter((candidate) => !paths.some((other) => other !== candidate && candidate.startsWith(`${other}/`)));
|
|
1012
|
-
}
|
|
1013
|
-
function addDirectoryMount(node, dataModelPath, classify, result) {
|
|
1014
|
-
const rawPath = node.$path;
|
|
1015
|
-
if (typeof rawPath !== "string") return;
|
|
1016
|
-
if (rawPath.endsWith(".project.json")) return;
|
|
1017
|
-
const fsPath = rawPath.replace(/\/$/, "");
|
|
1018
|
-
if (classify(fsPath) === "directory") result.push({
|
|
1019
|
-
dataModelPath,
|
|
1020
|
-
fsPath
|
|
1021
|
-
});
|
|
1022
|
-
}
|
|
1023
|
-
function isTreeChild(value) {
|
|
1024
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1025
|
-
}
|
|
1026
|
-
function walk(node, currentDataModelPath, classify, result) {
|
|
1027
|
-
for (const [key, value] of Object.entries(node)) {
|
|
1028
|
-
if (key.startsWith("$") || !isTreeChild(value)) continue;
|
|
1029
|
-
const childDataModelPath = currentDataModelPath === "" ? key : `${currentDataModelPath}/${key}`;
|
|
1030
|
-
addDirectoryMount(value, childDataModelPath, classify, result);
|
|
1031
|
-
walk(value, childDataModelPath, classify, result);
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
function matchNodePath(childNode, targetPath, childDataModelPath) {
|
|
1035
|
-
const nodePath = childNode.$path;
|
|
1036
|
-
if (typeof nodePath !== "string") return;
|
|
1037
|
-
const normalizedNodePath = nodePath.replace(/\/$/, "");
|
|
1038
|
-
if (normalizedNodePath === targetPath) return childDataModelPath;
|
|
1039
|
-
if (targetPath.startsWith(`${normalizedNodePath}/`)) return `${childDataModelPath}/${targetPath.slice(normalizedNodePath.length + 1)}`;
|
|
1040
|
-
}
|
|
1041
|
-
function findInTree(node, targetPath, currentDataModelPath) {
|
|
1042
|
-
for (const [key, value] of Object.entries(node)) {
|
|
1043
|
-
if (key.startsWith("$") || typeof value !== "object") continue;
|
|
1044
|
-
const childNode = value;
|
|
1045
|
-
const childDataModelPath = currentDataModelPath === "" ? key : `${currentDataModelPath}/${key}`;
|
|
1046
|
-
const pathMatch = matchNodePath(childNode, targetPath, childDataModelPath);
|
|
1047
|
-
if (pathMatch !== void 0) return pathMatch;
|
|
1048
|
-
const found = findInTree(childNode, targetPath, childDataModelPath);
|
|
1049
|
-
if (found !== void 0) return found;
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
//#endregion
|
|
1053
|
-
//#region src/utils/normalize-windows-path.ts
|
|
1054
|
-
const DRIVE_LETTER_START_REGEX = /^[A-Za-z]:\//;
|
|
1055
|
-
function normalizeWindowsPath(input = "") {
|
|
1056
|
-
if (!input) return input;
|
|
1057
|
-
return input.replace(/\\/g, "/").replace(DRIVE_LETTER_START_REGEX, (driveLetterMatch) => driveLetterMatch.toUpperCase());
|
|
1058
|
-
}
|
|
1059
|
-
//#endregion
|
|
1060
|
-
//#region src/utils/tsconfig-mapping.ts
|
|
1061
|
-
function findMapping(filePath, mappings, key = "outDir") {
|
|
1062
|
-
let best;
|
|
1063
|
-
let bestLength = -1;
|
|
1064
|
-
for (const mapping of mappings) {
|
|
1065
|
-
const prefix = mapping[key];
|
|
1066
|
-
if ((filePath === prefix || filePath.startsWith(`${prefix}/`)) && prefix.length > bestLength) {
|
|
1067
|
-
best = mapping;
|
|
1068
|
-
bestLength = prefix.length;
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
return best;
|
|
1072
|
-
}
|
|
1073
|
-
function replacePrefix(filePath, from, to) {
|
|
1074
|
-
if (filePath === from) return to;
|
|
1075
|
-
if (filePath.startsWith(`${from}/`)) return `${to}${filePath.slice(from.length)}`;
|
|
1076
|
-
return filePath;
|
|
1077
|
-
}
|
|
1078
|
-
//#endregion
|
|
1079
|
-
//#region src/source-mapper/column-finder.ts
|
|
1080
|
-
/**
|
|
1081
|
-
* Finds the column position of the failing matcher in an expect() call. Returns
|
|
1082
|
-
* 1-indexed column position, or undefined if no expect is found.
|
|
1083
|
-
*/
|
|
1084
|
-
function findExpectationColumn(lineText) {
|
|
1085
|
-
if (!lineText) return;
|
|
1086
|
-
const expectIndex = lineText.search(/\bexpect\s*[.(]/);
|
|
1087
|
-
if (expectIndex === -1) return;
|
|
1088
|
-
const afterExpect = lineText.slice(expectIndex);
|
|
1089
|
-
const matcherRegex = /[.:]\s*([A-Za-z_$][\w$]*)\s*(?=\()/g;
|
|
1090
|
-
let lastMatcher = null;
|
|
1091
|
-
for (const match of afterExpect.matchAll(matcherRegex)) lastMatcher = match;
|
|
1092
|
-
const matcherName = lastMatcher?.[1];
|
|
1093
|
-
if (lastMatcher === null || matcherName === void 0) return;
|
|
1094
|
-
return expectIndex + (lastMatcher.index + lastMatcher[0].indexOf(matcherName)) + 1;
|
|
1095
|
-
}
|
|
1096
|
-
//#endregion
|
|
1097
|
-
//#region src/source-mapper/path-resolver.ts
|
|
1098
|
-
function createPathResolver(rojoProject, config) {
|
|
1099
|
-
const rojoMappings = /* @__PURE__ */ new Map();
|
|
1100
|
-
function walkTree(tree, prefix) {
|
|
1101
|
-
for (const [key, value] of Object.entries(tree)) {
|
|
1102
|
-
if (key.startsWith("$") || typeof value !== "object") continue;
|
|
1103
|
-
const dataModelPath = prefix ? `${prefix}.${key}` : key;
|
|
1104
|
-
const node = value;
|
|
1105
|
-
if (typeof node.$path === "string") rojoMappings.set(dataModelPath, node.$path);
|
|
1106
|
-
walkTree(node, dataModelPath);
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
walkTree(rojoProject.tree, "");
|
|
1110
|
-
const tsconfigMappings = config?.mappings ?? [];
|
|
1111
|
-
const sortedRojoMappings = [...rojoMappings.entries()].sort(([a], [b]) => b.length - a.length);
|
|
1112
|
-
return { resolve(dataModelPath) {
|
|
1113
|
-
for (const [prefix, basePath] of sortedRojoMappings) {
|
|
1114
|
-
if (dataModelPath !== prefix && !dataModelPath.startsWith(`${prefix}.`)) continue;
|
|
1115
|
-
const result = `${basePath}/${convertToFilePath(dataModelPath.slice(prefix.length + 1))}`;
|
|
1116
|
-
const mapping = findMapping(result, tsconfigMappings);
|
|
1117
|
-
if (mapping !== void 0) return {
|
|
1118
|
-
filePath: `${luauInitToIndex(replacePrefix(result, mapping.outDir, mapping.rootDir).replace(/^\.\//, ""))}.ts`,
|
|
1119
|
-
mapping
|
|
1120
|
-
};
|
|
1121
|
-
return { filePath: findLuaFile(result) };
|
|
1122
|
-
}
|
|
1123
|
-
} };
|
|
1124
|
-
}
|
|
1125
|
-
function convertToFilePath(suffix) {
|
|
1126
|
-
const parts = suffix.split(".");
|
|
1127
|
-
const result = [];
|
|
1128
|
-
for (let index = 0; index < parts.length; index++) {
|
|
1129
|
-
const part = parts[index];
|
|
1130
|
-
const nextPart = parts[index + 1];
|
|
1131
|
-
if ((nextPart === "spec" || nextPart === "test") && index + 2 === parts.length) {
|
|
1132
|
-
result.push(`${part}.${nextPart}`);
|
|
1133
|
-
index++;
|
|
1134
|
-
} else result.push(part);
|
|
1135
|
-
}
|
|
1136
|
-
return result.join("/");
|
|
1137
|
-
}
|
|
1138
|
-
/** roblox-ts compiles index.ts → init.luau; reverse the rename for TS paths. */
|
|
1139
|
-
function luauInitToIndex(filePath) {
|
|
1140
|
-
return filePath.replace(/(^|\/)(init)(\.|\/)/, "$1index$3");
|
|
1141
|
-
}
|
|
1142
|
-
function findLuaFile(basePath) {
|
|
1143
|
-
const luauPath = `${basePath}.luau`;
|
|
1144
|
-
if (existsSync(luauPath)) return luauPath;
|
|
1145
|
-
const luaPath = `${basePath}.lua`;
|
|
1146
|
-
if (existsSync(luaPath)) return luaPath;
|
|
1147
|
-
return luauPath;
|
|
1148
|
-
}
|
|
1149
|
-
//#endregion
|
|
1150
|
-
//#region src/source-mapper/stack-parser.ts
|
|
1151
|
-
const FRAME_REGEX = /\[string "([^"]+)"\]:(\d+)(?::(\d+))?/g;
|
|
1152
|
-
function parseStack(input) {
|
|
1153
|
-
const frames = [];
|
|
1154
|
-
let firstMatchIndex = input.length;
|
|
1155
|
-
for (const match of input.matchAll(FRAME_REGEX)) {
|
|
1156
|
-
if (match.index < firstMatchIndex) firstMatchIndex = match.index;
|
|
1157
|
-
frames.push({
|
|
1158
|
-
column: match[3] !== void 0 ? Number(match[3]) : void 0,
|
|
1159
|
-
dataModelPath: String(match[1]),
|
|
1160
|
-
line: Number(match[2])
|
|
1161
|
-
});
|
|
1162
|
-
}
|
|
1163
|
-
return {
|
|
1164
|
-
frames,
|
|
1165
|
-
message: input.slice(0, firstMatchIndex).trim()
|
|
1166
|
-
};
|
|
1167
|
-
}
|
|
1168
|
-
//#endregion
|
|
1169
|
-
//#region src/source-mapper/v3-mapper.ts
|
|
1170
|
-
const mapCache = /* @__PURE__ */ new Map();
|
|
1171
|
-
function getSourceContent(luauPath, source) {
|
|
1172
|
-
const traced = getTraceMap(luauPath);
|
|
1173
|
-
if (traced === void 0) return;
|
|
1174
|
-
return sourceContentFor(traced, source);
|
|
1175
|
-
}
|
|
1176
|
-
function mapFromSourceMap(luauPath, luauLine, luauColumn = 0) {
|
|
1177
|
-
const traced = getTraceMap(luauPath);
|
|
1178
|
-
if (traced === void 0) return;
|
|
1179
|
-
const result = originalPositionFor(traced, {
|
|
1180
|
-
column: luauColumn,
|
|
1181
|
-
line: luauLine
|
|
1182
|
-
});
|
|
1183
|
-
if (result.line === null) return;
|
|
1184
|
-
return result;
|
|
1185
|
-
}
|
|
1186
|
-
function getTraceMap(luauPath) {
|
|
1187
|
-
let traced = mapCache.get(luauPath);
|
|
1188
|
-
if (traced !== void 0) return traced;
|
|
1189
|
-
const mapPath = `${luauPath}.map`;
|
|
1190
|
-
if (!fs$1.existsSync(mapPath)) return;
|
|
1191
|
-
traced = new TraceMap(fs$1.readFileSync(mapPath, "utf-8"));
|
|
1192
|
-
mapCache.set(luauPath, traced);
|
|
1193
|
-
return traced;
|
|
1194
|
-
}
|
|
1195
|
-
//#endregion
|
|
1196
|
-
//#region src/source-mapper/index.ts
|
|
1197
|
-
function createSourceMapper(config) {
|
|
1198
|
-
const pathResolver = createPathResolver(config.rojoProject, { mappings: config.mappings });
|
|
1199
|
-
function mapFrame(frame) {
|
|
1200
|
-
const resolved = pathResolver.resolve(frame.dataModelPath);
|
|
1201
|
-
if (resolved === void 0) return;
|
|
1202
|
-
if (resolved.mapping === void 0) return {
|
|
1203
|
-
luauPath: resolved.filePath,
|
|
1204
|
-
mapped: void 0
|
|
1205
|
-
};
|
|
1206
|
-
const { filePath, mapping } = resolved;
|
|
1207
|
-
const luauPath = replacePrefix(filePath, mapping.rootDir, mapping.outDir).replace(/\.ts$/, ".luau");
|
|
1208
|
-
const v3Result = mapFromSourceMap(luauPath, frame.line, frame.column);
|
|
1209
|
-
if (v3Result !== void 0 && v3Result.source !== null && v3Result.line !== null) {
|
|
1210
|
-
const mapDirectory = path$1.dirname(luauPath);
|
|
1211
|
-
const resolvedTsPath = normalizeWindowsPath(path$1.resolve(mapDirectory, v3Result.source));
|
|
1212
|
-
const tsLine = v3Result.line;
|
|
1213
|
-
const embeddedContent = getSourceContent(luauPath, v3Result.source) ?? void 0;
|
|
1214
|
-
const tsContent = embeddedContent ?? (fs$1.existsSync(resolvedTsPath) ? fs$1.readFileSync(resolvedTsPath, "utf-8") : void 0);
|
|
1215
|
-
return {
|
|
1216
|
-
luauPath,
|
|
1217
|
-
mapped: {
|
|
1218
|
-
column: tsContent !== void 0 ? findExpectationColumn(tsContent.split("\n")[tsLine - 1] ?? "") : void 0,
|
|
1219
|
-
line: tsLine,
|
|
1220
|
-
path: resolvedTsPath,
|
|
1221
|
-
sourceContent: embeddedContent
|
|
1222
|
-
}
|
|
1223
|
-
};
|
|
1224
|
-
}
|
|
1225
|
-
return {
|
|
1226
|
-
luauPath,
|
|
1227
|
-
mapped: void 0
|
|
1228
|
-
};
|
|
1229
|
-
}
|
|
1230
|
-
return {
|
|
1231
|
-
mapFailureMessage(message) {
|
|
1232
|
-
const parsed = parseStack(message);
|
|
1233
|
-
let result = message;
|
|
1234
|
-
for (const frame of parsed.frames) {
|
|
1235
|
-
const frameResult = mapFrame(frame);
|
|
1236
|
-
if (frameResult === void 0) continue;
|
|
1237
|
-
const original = `[string "${frame.dataModelPath}"]:${frame.line}`;
|
|
1238
|
-
if (frameResult.mapped !== void 0) {
|
|
1239
|
-
const mapped = `${frameResult.mapped.path}:${frameResult.mapped.line}`;
|
|
1240
|
-
result = result.replace(original, mapped);
|
|
1241
|
-
} else {
|
|
1242
|
-
const replacement = `${frameResult.luauPath}:${frame.line}`;
|
|
1243
|
-
result = result.replace(original, replacement);
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
return result;
|
|
1247
|
-
},
|
|
1248
|
-
mapFailureWithLocations(message) {
|
|
1249
|
-
const parsed = parseStack(message);
|
|
1250
|
-
let mappedMessage = message;
|
|
1251
|
-
const locations = [];
|
|
1252
|
-
for (const frame of parsed.frames) {
|
|
1253
|
-
const frameResult = mapFrame(frame);
|
|
1254
|
-
if (frameResult === void 0) continue;
|
|
1255
|
-
const original = `[string "${frame.dataModelPath}"]:${frame.line}`;
|
|
1256
|
-
if (frameResult.mapped !== void 0) {
|
|
1257
|
-
const mapped = `${frameResult.mapped.path}:${frameResult.mapped.line}`;
|
|
1258
|
-
mappedMessage = mappedMessage.replace(original, mapped);
|
|
1259
|
-
locations.push({
|
|
1260
|
-
luauLine: frame.line,
|
|
1261
|
-
luauPath: frameResult.luauPath,
|
|
1262
|
-
sourceContent: frameResult.mapped.sourceContent,
|
|
1263
|
-
tsColumn: frameResult.mapped.column,
|
|
1264
|
-
tsLine: frameResult.mapped.line,
|
|
1265
|
-
tsPath: frameResult.mapped.path
|
|
1266
|
-
});
|
|
1267
|
-
} else {
|
|
1268
|
-
const replacement = `${frameResult.luauPath}:${frame.line}`;
|
|
1269
|
-
mappedMessage = mappedMessage.replace(original, replacement);
|
|
1270
|
-
if (locations.length === 0) locations.push({
|
|
1271
|
-
luauLine: frame.line,
|
|
1272
|
-
luauPath: frameResult.luauPath
|
|
1273
|
-
});
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
return {
|
|
1277
|
-
locations,
|
|
1278
|
-
message: mappedMessage
|
|
1279
|
-
};
|
|
1280
|
-
},
|
|
1281
|
-
resolveTestFilePath(testFilePath) {
|
|
1282
|
-
const dataModelPath = testFilePath.replace(/^\//, "").replaceAll("/", ".");
|
|
1283
|
-
return pathResolver.resolve(dataModelPath)?.filePath;
|
|
1284
|
-
}
|
|
1285
|
-
};
|
|
1286
|
-
}
|
|
1287
|
-
/**
|
|
1288
|
-
* Compose multiple `SourceMapper`s into one that tries every child in order.
|
|
1289
|
-
* Used by the multi-project CLI path so that failure messages and GitHub
|
|
1290
|
-
* annotations can resolve frames from any project's TS/Luau mapping.
|
|
1291
|
-
*
|
|
1292
|
-
* Each child mapper only rewrites frames it can resolve, leaving the rest
|
|
1293
|
-
* untouched. Chaining `mapFailureMessage` / `mapFailureWithLocations` calls
|
|
1294
|
-
* through every child is therefore safe: later mappers see the partially
|
|
1295
|
-
* rewritten string and still parse any remaining `[string "..."]` frames.
|
|
1296
|
-
* Locations accumulate across mappers; `resolveTestFilePath` returns the
|
|
1297
|
-
* first child's hit.
|
|
1298
|
-
*/
|
|
1299
|
-
function combineSourceMappers(mappers) {
|
|
1300
|
-
if (mappers.length === 0) return;
|
|
1301
|
-
if (mappers.length === 1) return mappers[0];
|
|
1302
|
-
return {
|
|
1303
|
-
mapFailureMessage(message) {
|
|
1304
|
-
let result = message;
|
|
1305
|
-
for (const mapper of mappers) result = mapper.mapFailureMessage(result);
|
|
1306
|
-
return result;
|
|
1307
|
-
},
|
|
1308
|
-
mapFailureWithLocations(message) {
|
|
1309
|
-
let mappedMessage = message;
|
|
1310
|
-
const locations = [];
|
|
1311
|
-
for (const mapper of mappers) {
|
|
1312
|
-
const partial = mapper.mapFailureWithLocations(mappedMessage);
|
|
1313
|
-
mappedMessage = partial.message;
|
|
1314
|
-
locations.push(...partial.locations);
|
|
1315
|
-
}
|
|
1316
|
-
return {
|
|
1317
|
-
locations,
|
|
1318
|
-
message: mappedMessage
|
|
1319
|
-
};
|
|
1320
|
-
},
|
|
1321
|
-
resolveTestFilePath(testFilePath) {
|
|
1322
|
-
for (const mapper of mappers) {
|
|
1323
|
-
const resolved = mapper.resolveTestFilePath(testFilePath);
|
|
1324
|
-
if (resolved !== void 0) return resolved;
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
};
|
|
1328
|
-
}
|
|
1329
|
-
function getSourceSnippet({ column, context = 2, filePath, line, sourceContent }) {
|
|
1330
|
-
const content = sourceContent ?? (fs$1.existsSync(filePath) ? fs$1.readFileSync(filePath, "utf-8") : void 0);
|
|
1331
|
-
if (content === void 0) return;
|
|
1332
|
-
const allLines = content.split("\n");
|
|
1333
|
-
const startLine = Math.max(1, line - context);
|
|
1334
|
-
const endLine = Math.min(allLines.length, line + context);
|
|
1335
|
-
const lines = [];
|
|
1336
|
-
for (let index = startLine; index <= endLine; index++) {
|
|
1337
|
-
const lineContent = allLines[index - 1];
|
|
1338
|
-
assert(lineContent !== void 0, `index ${index} out of bounds`);
|
|
1339
|
-
lines.push({
|
|
1340
|
-
content: lineContent,
|
|
1341
|
-
num: index
|
|
1342
|
-
});
|
|
1343
|
-
}
|
|
1344
|
-
const failureLineContent = allLines[line - 1] ?? "";
|
|
1345
|
-
return {
|
|
1346
|
-
column: column ?? findExpectationColumn(failureLineContent),
|
|
1347
|
-
failureLine: line,
|
|
1348
|
-
lines
|
|
1349
|
-
};
|
|
1350
|
-
}
|
|
1351
|
-
//#endregion
|
|
1352
|
-
//#region src/types/jest-result.ts
|
|
1353
|
-
function hasExecError(file) {
|
|
1354
|
-
return file.failureMessage !== void 0 && file.failureMessage !== "" && file.testResults.length === 0;
|
|
1355
|
-
}
|
|
1356
|
-
//#endregion
|
|
1357
|
-
//#region src/utils/banner.ts
|
|
1358
|
-
const SEPARATOR$1 = "⎯";
|
|
1359
|
-
const levelStyles = {
|
|
1360
|
-
error: {
|
|
1361
|
-
badge: (text) => color.bgRed(color.white(color.bold(text))),
|
|
1362
|
-
separator: color.red
|
|
1363
|
-
},
|
|
1364
|
-
warn: {
|
|
1365
|
-
badge: (text) => color.bgYellow(color.black(color.bold(text))),
|
|
1366
|
-
separator: color.yellow
|
|
1367
|
-
}
|
|
1368
|
-
};
|
|
1369
|
-
function formatBannerBar({ level, termWidth, title }) {
|
|
1370
|
-
const width = termWidth ?? getDefaultWidth();
|
|
1371
|
-
const styles = levelStyles[level];
|
|
1372
|
-
const badgeText = ` ${title} `;
|
|
1373
|
-
const badge = styles.badge(badgeText);
|
|
1374
|
-
const remaining = width - badgeText.length;
|
|
1375
|
-
const leftWidth = Math.max(1, Math.floor(remaining / 2));
|
|
1376
|
-
const rightWidth = Math.max(1, remaining - leftWidth);
|
|
1377
|
-
return `${styles.separator(SEPARATOR$1.repeat(leftWidth))}${badge}${styles.separator(SEPARATOR$1.repeat(rightWidth))}`;
|
|
1378
|
-
}
|
|
1379
|
-
function formatBanner({ body, level, termWidth, title }) {
|
|
1380
|
-
const width = termWidth ?? getDefaultWidth();
|
|
1381
|
-
const styles = levelStyles[level];
|
|
1382
|
-
const header = formatBannerBar({
|
|
1383
|
-
level,
|
|
1384
|
-
termWidth: width,
|
|
1385
|
-
title
|
|
1386
|
-
});
|
|
1387
|
-
const closing = styles.separator(SEPARATOR$1.repeat(width));
|
|
1388
|
-
return `\n${header}\n${body.length > 0 ? `\n${body.join("\n")}\n` : ""}\n${closing}\n\n`;
|
|
1389
|
-
}
|
|
1390
|
-
function getDefaultWidth() {
|
|
1391
|
-
return process.stderr.columns || 80;
|
|
1392
|
-
}
|
|
1393
|
-
//#endregion
|
|
1394
|
-
//#region src/highlighter/luau-grammar.ts
|
|
1395
|
-
const OPENING_LONG_BRACKET = "\\[=*\\[";
|
|
1396
|
-
const CLOSING_LONG_BRACKET = "\\]=*\\]";
|
|
1397
|
-
const BUILT_IN = "_G _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert __idiv __iter newproxy rawlen collectgarbage error getfenv getmetatable ipairs loadstring next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall self coroutine resume yield status wrap create running debug traceback math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os date difftime time clock string sub upper len rep find match char gmatch reverse byte format gsub lower table insert getn foreachi maxn foreach concat sort remove game workspace script plugin Instance Enum describe it expect test beforeAll afterAll beforeEach afterEach jest toBe toEqual toContain toThrow toHaveBeenCalled";
|
|
1398
|
-
const KEYWORD = "and break continue do else elseif end for function if in local not or repeat return then until while type export";
|
|
1399
|
-
function luauGrammar(hljs) {
|
|
1400
|
-
const longBrackets = {
|
|
1401
|
-
begin: OPENING_LONG_BRACKET,
|
|
1402
|
-
contains: ["self"],
|
|
1403
|
-
end: CLOSING_LONG_BRACKET
|
|
1404
|
-
};
|
|
1405
|
-
const comments = [hljs.COMMENT(`--(?!${OPENING_LONG_BRACKET})`, "$"), hljs.COMMENT(`--${OPENING_LONG_BRACKET}`, CLOSING_LONG_BRACKET, {
|
|
1406
|
-
contains: [longBrackets],
|
|
1407
|
-
relevance: 10
|
|
1408
|
-
})];
|
|
1409
|
-
return {
|
|
1410
|
-
name: "Luau",
|
|
1411
|
-
contains: [
|
|
1412
|
-
...comments,
|
|
1413
|
-
{
|
|
1414
|
-
beginKeywords: "function",
|
|
1415
|
-
contains: [
|
|
1416
|
-
hljs.inherit(hljs.TITLE_MODE, { begin: "([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*" }),
|
|
1417
|
-
{
|
|
1418
|
-
begin: "\\(",
|
|
1419
|
-
contains: comments,
|
|
1420
|
-
endsWithParent: true,
|
|
1421
|
-
scope: "params"
|
|
1422
|
-
},
|
|
1423
|
-
...comments
|
|
1424
|
-
],
|
|
1425
|
-
end: "\\)",
|
|
1426
|
-
scope: "function"
|
|
1427
|
-
},
|
|
1428
|
-
hljs.C_NUMBER_MODE,
|
|
1429
|
-
hljs.APOS_STRING_MODE,
|
|
1430
|
-
hljs.QUOTE_STRING_MODE,
|
|
1431
|
-
{
|
|
1432
|
-
begin: OPENING_LONG_BRACKET,
|
|
1433
|
-
contains: [longBrackets],
|
|
1434
|
-
end: CLOSING_LONG_BRACKET,
|
|
1435
|
-
relevance: 5,
|
|
1436
|
-
scope: "string"
|
|
1437
|
-
},
|
|
1438
|
-
{
|
|
1439
|
-
begin: "`",
|
|
1440
|
-
contains: [{
|
|
1441
|
-
begin: "\\{",
|
|
1442
|
-
end: "\\}",
|
|
1443
|
-
scope: "subst"
|
|
1444
|
-
}],
|
|
1445
|
-
end: "`",
|
|
1446
|
-
scope: "string"
|
|
1447
|
-
}
|
|
1448
|
-
],
|
|
1449
|
-
keywords: {
|
|
1450
|
-
$pattern: hljs.UNDERSCORE_IDENT_RE,
|
|
1451
|
-
built_in: BUILT_IN,
|
|
1452
|
-
keyword: KEYWORD,
|
|
1453
|
-
literal: "true false nil"
|
|
1454
|
-
}
|
|
1455
|
-
};
|
|
1456
|
-
}
|
|
1457
|
-
//#endregion
|
|
1458
|
-
//#region src/utils/colors.ts
|
|
1459
|
-
hljs.registerLanguage("luau", luauGrammar);
|
|
1460
|
-
hljs.registerLanguage("typescript", typescript);
|
|
1461
|
-
const EXTENSION_NAME_REGEX = /.(\.[^./]+|\.)$/;
|
|
1462
|
-
const TS_SUPPORTED_EXTS = new Set(["js", "ts"].flatMap((lang) => [
|
|
1463
|
-
`.${lang}`,
|
|
1464
|
-
`.m${lang}`,
|
|
1465
|
-
`.c${lang}`,
|
|
1466
|
-
`.${lang}x`,
|
|
1467
|
-
`.m${lang}x`,
|
|
1468
|
-
`.c${lang}x`
|
|
1469
|
-
]));
|
|
1470
|
-
const LUAU_SUPPORTED_EXTS = new Set([".lua", ".luau"]);
|
|
1471
|
-
function highlightCode(id, source) {
|
|
1472
|
-
const extension = extname(id);
|
|
1473
|
-
if (LUAU_SUPPORTED_EXTS.has(extension)) return highlightLuau(source);
|
|
1474
|
-
if (TS_SUPPORTED_EXTS.has(extension)) return highlightTypeScript(source);
|
|
1475
|
-
return source;
|
|
1476
|
-
}
|
|
1477
|
-
function extname(path) {
|
|
1478
|
-
if (path === "..") return "";
|
|
1479
|
-
return EXTENSION_NAME_REGEX.exec(normalizeWindowsPath(path))?.[1] ?? "";
|
|
1480
|
-
}
|
|
1481
|
-
const HLJS_CLASS_TO_COLOR = {
|
|
1482
|
-
"hljs-attr": color.blue,
|
|
1483
|
-
"hljs-built_in": color.blue,
|
|
1484
|
-
"hljs-comment": color.gray,
|
|
1485
|
-
"hljs-function": color.blue,
|
|
1486
|
-
"hljs-keyword": color.magenta,
|
|
1487
|
-
"hljs-literal": color.blue,
|
|
1488
|
-
"hljs-meta": color.gray,
|
|
1489
|
-
"hljs-number": color.blue,
|
|
1490
|
-
"hljs-operator": color.yellow,
|
|
1491
|
-
"hljs-params": color.white,
|
|
1492
|
-
"hljs-punctuation": color.yellow,
|
|
1493
|
-
"hljs-regexp": color.cyan,
|
|
1494
|
-
"hljs-string": color.green,
|
|
1495
|
-
"hljs-subst": color.cyan,
|
|
1496
|
-
"hljs-title": color.blue,
|
|
1497
|
-
"hljs-type": color.yellow,
|
|
1498
|
-
"hljs-variable": color.white
|
|
1499
|
-
};
|
|
1500
|
-
function convertHljsToAnsi(html) {
|
|
1501
|
-
let result = html;
|
|
1502
|
-
let previous = "";
|
|
1503
|
-
while (result !== previous) {
|
|
1504
|
-
previous = result;
|
|
1505
|
-
result = result.replace(/<span class="([^"]+)">([^<]*)<\/span>/g, (_, cssClasses, content) => {
|
|
1506
|
-
const primaryClass = String(cssClasses).split(" ")[0];
|
|
1507
|
-
assert(primaryClass !== void 0, "split always returns ≥1 element");
|
|
1508
|
-
const text = String(content);
|
|
1509
|
-
const colorFunc = HLJS_CLASS_TO_COLOR[primaryClass];
|
|
1510
|
-
return colorFunc?.(text) ?? text;
|
|
1511
|
-
});
|
|
1512
|
-
}
|
|
1513
|
-
return result.replace(/"/g, "\"").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1514
|
-
}
|
|
1515
|
-
function highlightLuau(source) {
|
|
1516
|
-
return convertHljsToAnsi(hljs.highlight(source, { language: "luau" }).value);
|
|
1517
|
-
}
|
|
1518
|
-
function highlightTypeScript(source) {
|
|
1519
|
-
return convertHljsToAnsi(hljs.highlight(source, { language: "typescript" }).value).replace(/=>/g, color.yellow("=>"));
|
|
1520
|
-
}
|
|
1521
|
-
//#endregion
|
|
1522
|
-
//#region src/formatters/formatter.ts
|
|
1523
|
-
const EXEC_ERROR_HINTS = [[/loadstring\(\) is not available/, "loadstring() must be enabled for Jest to run. Add to your project.json:\n\n \"ServerScriptService\": {\n \"$properties\": {\n \"LoadStringEnabled\": true\n }\n }"]];
|
|
1524
|
-
function getExecErrorHint(message) {
|
|
1525
|
-
for (const [pattern, hint] of EXEC_ERROR_HINTS) if (pattern.test(message)) return hint;
|
|
1526
|
-
}
|
|
1527
|
-
function formatFailedTestsHeader(failCount, _styles) {
|
|
1528
|
-
return formatBannerBar({
|
|
1529
|
-
level: "error",
|
|
1530
|
-
termWidth: getTerminalWidth(),
|
|
1531
|
-
title: `Failed Tests ${failCount}`
|
|
1532
|
-
});
|
|
1533
|
-
}
|
|
1534
|
-
function parseErrorMessage(message) {
|
|
1535
|
-
const lines = message.split("\n");
|
|
1536
|
-
const firstLine = lines[0];
|
|
1537
|
-
assert(firstLine !== void 0, "split always returns ≥1 element");
|
|
1538
|
-
const snapshotHeaderIndex = lines.findIndex((line) => /^- Snapshot\s+- \d+/.test(line));
|
|
1539
|
-
if (snapshotHeaderIndex !== -1) {
|
|
1540
|
-
const diffLines = [];
|
|
1541
|
-
for (let index = snapshotHeaderIndex; index < lines.length; index++) {
|
|
1542
|
-
const line = lines[index];
|
|
1543
|
-
if (line.startsWith("[string ")) break;
|
|
1544
|
-
diffLines.push(line);
|
|
1545
|
-
}
|
|
1546
|
-
return {
|
|
1547
|
-
message: firstLine,
|
|
1548
|
-
snapshotDiff: diffLines.join("\n").trimEnd()
|
|
1549
|
-
};
|
|
1550
|
-
}
|
|
1551
|
-
const expectedMatch = message.match(/Expected\b.*?:\s*(.+)/);
|
|
1552
|
-
const receivedMatch = message.match(/Received\b.*?:\s*(.+)/);
|
|
1553
|
-
return {
|
|
1554
|
-
expected: expectedMatch?.[1],
|
|
1555
|
-
message: firstLine,
|
|
1556
|
-
received: receivedMatch?.[1]
|
|
1557
|
-
};
|
|
1558
|
-
}
|
|
1559
|
-
/**
|
|
1560
|
-
* Extracts the meaningful error message from a Jest `failureMessage` string.
|
|
1561
|
-
* Strips the "● Test suite failed to run" header, Roblox DataModel path chains,
|
|
1562
|
-
* and stack trace lines.
|
|
1563
|
-
*/
|
|
1564
|
-
function cleanExecErrorMessage(raw) {
|
|
1565
|
-
if (raw === "") return "";
|
|
1566
|
-
const lines = raw.split("\n");
|
|
1567
|
-
let contentLine;
|
|
1568
|
-
let pastHeader = false;
|
|
1569
|
-
for (const line of lines) {
|
|
1570
|
-
const trimmed = line.trim();
|
|
1571
|
-
if (trimmed.startsWith("●")) {
|
|
1572
|
-
pastHeader = true;
|
|
1573
|
-
continue;
|
|
1574
|
-
}
|
|
1575
|
-
if (pastHeader && trimmed !== "") {
|
|
1576
|
-
contentLine = trimmed;
|
|
1577
|
-
break;
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
if (contentLine === void 0) return raw.trim();
|
|
1581
|
-
return contentLine.replace(/^(?:[A-Za-z][\w.@-]*:\d+:\s*)+/, "");
|
|
1582
|
-
}
|
|
1583
|
-
function formatSourceSnippet(snippet, filePath, options) {
|
|
1584
|
-
const useColor = options?.useColor ?? true;
|
|
1585
|
-
const styles = options?.styles ?? createStyles(useColor);
|
|
1586
|
-
const language = options?.language;
|
|
1587
|
-
const lines = [];
|
|
1588
|
-
const indent = " ";
|
|
1589
|
-
const location = snippet.column !== void 0 ? `${filePath}:${snippet.failureLine}:${snippet.column}` : `${filePath}:${snippet.failureLine}`;
|
|
1590
|
-
const langSuffix = language !== void 0 ? styles.dim(` (${language})`) : "";
|
|
1591
|
-
lines.push(styles.location(` ❯ ${location}`) + langSuffix);
|
|
1592
|
-
const maxLineNumber = Math.max(...snippet.lines.map((line) => line.num));
|
|
1593
|
-
const padding = String(maxLineNumber).length;
|
|
1594
|
-
for (const line of snippet.lines) {
|
|
1595
|
-
const prefix = `${String(line.num).padStart(padding)}|`;
|
|
1596
|
-
const highlighted = highlightSyntax(filePath, expandTabs(line.content), useColor);
|
|
1597
|
-
if (line.num === snippet.failureLine) {
|
|
1598
|
-
lines.push(`${indent}${styles.lineNumber(prefix)} ${highlighted}`);
|
|
1599
|
-
if (snippet.column !== void 0) {
|
|
1600
|
-
const beforeColumn = expandTabs(line.content.slice(0, snippet.column - 1));
|
|
1601
|
-
const caretGutter = `${" ".repeat(padding)}|`;
|
|
1602
|
-
const gutterPrefix = styles.lineNumber(caretGutter);
|
|
1603
|
-
lines.push(`${indent}${gutterPrefix} ${" ".repeat(beforeColumn.length)}${styles.status.fail("^")}`);
|
|
1604
|
-
}
|
|
1605
|
-
} else lines.push(`${indent}${styles.lineNumber(prefix)} ${highlighted}`);
|
|
1606
|
-
}
|
|
1607
|
-
return lines.join("\n");
|
|
1608
|
-
}
|
|
1609
|
-
function parseSourceLocation(message) {
|
|
1610
|
-
const match = message.match(/([^\s:]+\.(?:tsx?|luau?)):(\d+)(?::(\d+))?/);
|
|
1611
|
-
if (match === null) return;
|
|
1612
|
-
const [, filePath, lineStr, columnStr] = match;
|
|
1613
|
-
assert(filePath !== void 0, "regex group 1 matched");
|
|
1614
|
-
assert(lineStr !== void 0, "regex group 2 matched");
|
|
1615
|
-
return {
|
|
1616
|
-
column: columnStr !== void 0 ? Number.parseInt(columnStr, 10) : void 0,
|
|
1617
|
-
line: Number.parseInt(lineStr, 10),
|
|
1618
|
-
path: filePath
|
|
1619
|
-
};
|
|
1620
|
-
}
|
|
1621
|
-
function resolveDisplayPath(testFilePath, sourceMapper) {
|
|
1622
|
-
return sourceMapper?.resolveTestFilePath(testFilePath) ?? testFilePath;
|
|
1623
|
-
}
|
|
1624
|
-
function formatFailure({ failureIndex, filePath, showLuau = false, sourceMapper, styles, test, totalFailures, useColor = true }) {
|
|
1625
|
-
const st = styles ?? createStyles(useColor);
|
|
1626
|
-
const lines = [];
|
|
1627
|
-
const pathParts = filePath !== void 0 ? [filePath] : [];
|
|
1628
|
-
pathParts.push(...test.ancestorTitles, test.title);
|
|
1629
|
-
const testPath = pathParts.join(" > ");
|
|
1630
|
-
lines.push("", `${st.failBadge(" FAIL ")} ${st.status.fail(testPath)}`);
|
|
1631
|
-
for (const originalMessage of test.failureMessages) lines.push(...formatFailureMessage(originalMessage, {
|
|
1632
|
-
filePath,
|
|
1633
|
-
showLuau,
|
|
1634
|
-
sourceMapper,
|
|
1635
|
-
styles: st,
|
|
1636
|
-
useColor
|
|
1637
|
-
}));
|
|
1638
|
-
if (failureIndex !== void 0 && totalFailures !== void 0) {
|
|
1639
|
-
const counter = `[${failureIndex}/${totalFailures}]`;
|
|
1640
|
-
const termWidth = getTerminalWidth();
|
|
1641
|
-
const fillWidth = Math.max(1, termWidth - counter.length - 3);
|
|
1642
|
-
lines.push("", st.dim(st.status.fail(`${"⎯".repeat(fillWidth)}${counter}⎯`)));
|
|
1643
|
-
}
|
|
1644
|
-
return lines.map((line) => ` ${line}`).join("\n");
|
|
1645
|
-
}
|
|
1646
|
-
function formatRunHeader(options, styles) {
|
|
1647
|
-
const st = styles ?? createStyles(options.color);
|
|
1648
|
-
const header = `\n${st.runBadge(" RUN ")} ${st.location(`v${options.version}`)} ${st.lineNumber(options.rootDir)}`;
|
|
1649
|
-
if (options.collectCoverage === true) return `${header}\n${`${st.dim(" Coverage enabled with")} ${st.status.pending("istanbul")}`}\n`;
|
|
1650
|
-
return `${header}\n`;
|
|
1651
|
-
}
|
|
1652
|
-
function formatTestSummary(result, timing, styles, options) {
|
|
1653
|
-
const st = styles ?? createStyles(true);
|
|
1654
|
-
const lines = [];
|
|
1655
|
-
const execErrorFiles = result.testResults.filter(hasExecError).length;
|
|
1656
|
-
const totalFiles = result.testResults.length;
|
|
1657
|
-
const failedFiles = result.testResults.filter((fr) => fr.numFailingTests > 0).length + execErrorFiles;
|
|
1658
|
-
const skippedFiles = result.testResults.filter((fr) => fr.numFailingTests === 0 && fr.numPassingTests === 0 && !hasExecError(fr)).length;
|
|
1659
|
-
const passedFiles = totalFiles - failedFiles - skippedFiles;
|
|
1660
|
-
const fileParts = [];
|
|
1661
|
-
if (passedFiles > 0) fileParts.push(st.summary.passed(`${passedFiles} passed`));
|
|
1662
|
-
if (failedFiles > 0) fileParts.push(st.summary.failed(`${failedFiles} failed`));
|
|
1663
|
-
if (skippedFiles > 0) fileParts.push(st.summary.pending(`${skippedFiles} skipped`));
|
|
1664
|
-
const snapshotLine = formatSnapshotLine(result.snapshot, st);
|
|
1665
|
-
if (snapshotLine !== void 0) lines.push(snapshotLine);
|
|
1666
|
-
const fileTotalLabel = st.dim(`(${totalFiles})`);
|
|
1667
|
-
lines.push(`${st.dim(" Test Files")} ${fileParts.join(" | ")} ${fileTotalLabel}`);
|
|
1668
|
-
const testParts = [];
|
|
1669
|
-
if (result.numPassedTests > 0) testParts.push(st.summary.passed(`${result.numPassedTests} passed`));
|
|
1670
|
-
if (result.numFailedTests > 0) testParts.push(st.summary.failed(`${result.numFailedTests} failed`));
|
|
1671
|
-
if (result.numPendingTests > 0) testParts.push(st.summary.pending(`${result.numPendingTests} skipped`));
|
|
1672
|
-
const testTotalLabel = st.dim(`(${result.numTotalTests})`);
|
|
1673
|
-
lines.push(`${st.dim(" Tests")} ${testParts.join(" | ")} ${testTotalLabel}`);
|
|
1674
|
-
if (options?.typeErrors !== void 0) {
|
|
1675
|
-
const typeErrorLabel = st.dim("Type Errors");
|
|
1676
|
-
const typeErrorValue = options.typeErrors > 0 ? st.summary.failed(`${options.typeErrors} failed`) : st.dim("no errors");
|
|
1677
|
-
lines.push(`${typeErrorLabel} ${typeErrorValue}`);
|
|
1678
|
-
}
|
|
1679
|
-
const startAtStr = new Date(timing.startTime).toLocaleTimeString("en-GB", { hour12: false });
|
|
1680
|
-
lines.push(`${st.dim(" Start at")} ${startAtStr}`);
|
|
1681
|
-
const setupMs = timing.setupMs ?? 0;
|
|
1682
|
-
const environmentMs = Math.max(0, timing.executionMs - timing.testsMs - setupMs);
|
|
1683
|
-
const uploadMs = timing.uploadMs ?? 0;
|
|
1684
|
-
const coverageMs = timing.coverageMs ?? 0;
|
|
1685
|
-
const cliMs = Math.max(0, timing.totalMs - uploadMs - timing.executionMs - coverageMs);
|
|
1686
|
-
const breakdownParts = [];
|
|
1687
|
-
if (timing.uploadMs !== void 0) breakdownParts.push(timing.uploadCached === true ? `upload ${timing.uploadMs}ms (cached)` : `upload ${timing.uploadMs}ms`);
|
|
1688
|
-
breakdownParts.push(`environment ${environmentMs}ms`);
|
|
1689
|
-
if (setupMs > 0) breakdownParts.push(`setup ${setupMs}ms`);
|
|
1690
|
-
breakdownParts.push(`tests ${timing.testsMs}ms`, `cli ${cliMs}ms`);
|
|
1691
|
-
if (coverageMs > 0) breakdownParts.push(`coverage ${coverageMs}ms`);
|
|
1692
|
-
const breakdown = st.dim(`(${breakdownParts.join(", ")})`);
|
|
1693
|
-
lines.push(`${st.dim(" Duration")} ${timing.totalMs}ms ${breakdown}`);
|
|
1694
|
-
return lines.join("\n");
|
|
1695
|
-
}
|
|
1696
|
-
function formatResult(result, timing, options) {
|
|
1697
|
-
const styles = createStyles(options.color);
|
|
1698
|
-
const lines = [formatRunHeader(options, styles)];
|
|
1699
|
-
for (const file of result.testResults) {
|
|
1700
|
-
if (options.failuresOnly === true && file.numFailingTests === 0 && !hasExecError(file)) continue;
|
|
1701
|
-
lines.push(formatFileSummary(file, options, styles));
|
|
1702
|
-
}
|
|
1703
|
-
const execErrors = result.testResults.filter(hasExecError);
|
|
1704
|
-
const totalDetailedFailures = result.numFailedTests + execErrors.length;
|
|
1705
|
-
if (totalDetailedFailures > 0) {
|
|
1706
|
-
lines.push("", formatFailedTestsHeader(totalDetailedFailures, styles));
|
|
1707
|
-
const failureCtx = {
|
|
1708
|
-
currentIndex: 1,
|
|
1709
|
-
totalFailures: totalDetailedFailures
|
|
1710
|
-
};
|
|
1711
|
-
for (const file of result.testResults) {
|
|
1712
|
-
const failures = formatFileFailures(file, options, styles, failureCtx);
|
|
1713
|
-
if (failures !== "") lines.push(failures);
|
|
1714
|
-
}
|
|
1715
|
-
for (const file of execErrors) lines.push(formatExecErrorDetail(file, styles, failureCtx, options.sourceMapper));
|
|
1716
|
-
}
|
|
1717
|
-
lines.push("", formatTestSummary(result, timing, styles, { typeErrors: options.typeErrors }));
|
|
1718
|
-
if (!result.success) {
|
|
1719
|
-
const hints = formatLogHints(options, styles);
|
|
1720
|
-
if (hints !== "") lines.push("", hints);
|
|
1721
|
-
}
|
|
1722
|
-
return lines.join("\n");
|
|
1723
|
-
}
|
|
1724
|
-
function formatTypecheckFailures(result, useColor = true) {
|
|
1725
|
-
const styles = createStyles(useColor);
|
|
1726
|
-
const lines = [];
|
|
1727
|
-
for (const file of result.testResults) for (const test of file.testResults) {
|
|
1728
|
-
if (test.status !== "failed") continue;
|
|
1729
|
-
const badge = styles.failBadge(" FAIL ");
|
|
1730
|
-
lines.push(` ${badge} ${styles.status.fail(test.fullName)}`);
|
|
1731
|
-
for (const message of test.failureMessages) lines.push(` ${styles.dim(message)}`);
|
|
1732
|
-
}
|
|
1733
|
-
return lines.join("\n");
|
|
1734
|
-
}
|
|
1735
|
-
function formatTypecheckSummary(result, useColor = true) {
|
|
1736
|
-
const styles = createStyles(useColor);
|
|
1737
|
-
const passed = result.numPassedTests;
|
|
1738
|
-
const failed = result.numFailedTests;
|
|
1739
|
-
const total = result.numTotalTests;
|
|
1740
|
-
const parts = [];
|
|
1741
|
-
if (failed > 0) parts.push(formatTypecheckFailures(result, useColor));
|
|
1742
|
-
const failedLabel = styles.summary.failed(`${String(failed)} failed`);
|
|
1743
|
-
const failedPart = failed > 0 ? `${failedLabel}, ` : "";
|
|
1744
|
-
const passedPart = styles.summary.passed(`${String(passed)} passed`);
|
|
1745
|
-
const label = styles.dim("Type Tests:");
|
|
1746
|
-
parts.push(`\n${label} ${failedPart}${passedPart}, ${String(total)} total\n`);
|
|
1747
|
-
return parts.join("\n");
|
|
1748
|
-
}
|
|
1749
|
-
const PROJECT_BADGE_COLORS = [
|
|
1750
|
-
(text) => color.bgYellow(color.black(text)),
|
|
1751
|
-
(text) => color.bgCyan(color.black(text)),
|
|
1752
|
-
(text) => color.bgGreen(color.black(text)),
|
|
1753
|
-
(text) => color.bgMagenta(color.black(text))
|
|
1754
|
-
];
|
|
1755
|
-
const NAMED_BADGE_COLORS = {
|
|
1756
|
-
blue: (text) => color.bgBlue(color.white(text)),
|
|
1757
|
-
cyan: (text) => color.bgCyan(color.black(text)),
|
|
1758
|
-
green: (text) => color.bgGreen(color.black(text)),
|
|
1759
|
-
magenta: (text) => color.bgMagenta(color.black(text)),
|
|
1760
|
-
red: (text) => color.bgRed(color.white(text)),
|
|
1761
|
-
white: (text) => color.bgWhite(color.black(text)),
|
|
1762
|
-
yellow: (text) => color.bgYellow(color.black(text))
|
|
1763
|
-
};
|
|
1764
|
-
function formatProjectBadge(displayName, useColor, displayColor) {
|
|
1765
|
-
if (!useColor) return `▶ ${displayName}`;
|
|
1766
|
-
return `▶ ${resolveBadgeColor(displayName, displayColor)(` ${displayName} `)}`;
|
|
1767
|
-
}
|
|
1768
|
-
function formatProjectHeader(options) {
|
|
1769
|
-
const { displayColor, displayName, result, styles: headerStyles, useColor = true } = options;
|
|
1770
|
-
const resolved = headerStyles ?? createStyles(useColor);
|
|
1771
|
-
const stats = computeProjectStats(result);
|
|
1772
|
-
const parts = [];
|
|
1773
|
-
if (stats.passedFiles > 0) parts.push(resolved.summary.passed(`${stats.passedFiles} passed`));
|
|
1774
|
-
if (stats.failedFiles > 0) parts.push(resolved.summary.failed(`${stats.failedFiles} failed`));
|
|
1775
|
-
if (stats.skippedFiles > 0) parts.push(resolved.summary.pending(`${stats.skippedFiles} skipped`));
|
|
1776
|
-
const duration = stats.durationMs > 0 ? ` - ${stats.durationMs}ms` : "";
|
|
1777
|
-
const meta = resolved.dim(`(${stats.totalTests} tests${duration})`);
|
|
1778
|
-
const fileStats = parts.join(" | ");
|
|
1779
|
-
return `${formatProjectBadge(displayName, useColor, displayColor)} ${fileStats} ${meta}`;
|
|
1780
|
-
}
|
|
1781
|
-
function formatProjectSection(section) {
|
|
1782
|
-
const { displayColor, displayName, failureCtx, options, result, styles: sectionStyles } = section;
|
|
1783
|
-
const resolved = sectionStyles ?? createStyles(options.color);
|
|
1784
|
-
const lines = [formatProjectHeader({
|
|
1785
|
-
displayColor,
|
|
1786
|
-
displayName,
|
|
1787
|
-
result,
|
|
1788
|
-
styles: resolved,
|
|
1789
|
-
useColor: options.color
|
|
1790
|
-
})];
|
|
1791
|
-
for (const file of result.testResults) {
|
|
1792
|
-
if (options.failuresOnly === true && file.numFailingTests === 0 && !hasExecError(file)) continue;
|
|
1793
|
-
lines.push(formatFileSummary(file, options, resolved));
|
|
1794
|
-
}
|
|
1795
|
-
const execErrors = result.testResults.filter(hasExecError);
|
|
1796
|
-
if (result.numFailedTests + execErrors.length > 0) {
|
|
1797
|
-
for (const file of result.testResults) {
|
|
1798
|
-
const failures = formatFileFailures(file, options, resolved, failureCtx);
|
|
1799
|
-
if (failures !== "") lines.push(failures);
|
|
1800
|
-
}
|
|
1801
|
-
for (const file of execErrors) lines.push(formatExecErrorDetail(file, resolved, failureCtx, options.sourceMapper));
|
|
1802
|
-
}
|
|
1803
|
-
return lines.join("\n");
|
|
1804
|
-
}
|
|
1805
|
-
function formatMultiProjectResult(projects, timing, options) {
|
|
1806
|
-
const styles = createStyles(options.color);
|
|
1807
|
-
let totalFailures = 0;
|
|
1808
|
-
for (const { result } of projects) {
|
|
1809
|
-
const execErrors = result.testResults.filter(hasExecError).length;
|
|
1810
|
-
totalFailures += result.numFailedTests + execErrors;
|
|
1811
|
-
}
|
|
1812
|
-
const failureCtx = {
|
|
1813
|
-
currentIndex: 1,
|
|
1814
|
-
totalFailures
|
|
1815
|
-
};
|
|
1816
|
-
const sections = [];
|
|
1817
|
-
for (const { displayColor, displayName, result } of projects) sections.push(formatProjectSection({
|
|
1818
|
-
displayColor,
|
|
1819
|
-
displayName,
|
|
1820
|
-
failureCtx,
|
|
1821
|
-
options,
|
|
1822
|
-
result,
|
|
1823
|
-
styles
|
|
1824
|
-
}));
|
|
1825
|
-
const lines = [formatRunHeader(options, styles), sections.join("\n\n")];
|
|
1826
|
-
const mergedResult = mergeJestResults(projects.map((project) => project.result));
|
|
1827
|
-
lines.push("", formatTestSummary(mergedResult, timing, styles, { typeErrors: options.typeErrors }));
|
|
1828
|
-
if (!mergedResult.success) {
|
|
1829
|
-
const hints = formatLogHints(options, styles);
|
|
1830
|
-
if (hints !== "") lines.push("", hints);
|
|
1831
|
-
}
|
|
1832
|
-
return lines.join("\n");
|
|
1833
|
-
}
|
|
1834
|
-
function hashProjectName(name) {
|
|
1835
|
-
let hash = 0;
|
|
1836
|
-
for (let index = 0; index < name.length; index++) hash += name.charCodeAt(index) + index;
|
|
1837
|
-
return hash % PROJECT_BADGE_COLORS.length;
|
|
1838
|
-
}
|
|
1839
|
-
function resolveBadgeColor(displayName, displayColor) {
|
|
1840
|
-
if (displayColor !== void 0) {
|
|
1841
|
-
const named = NAMED_BADGE_COLORS[displayColor];
|
|
1842
|
-
if (named !== void 0) return named;
|
|
1843
|
-
}
|
|
1844
|
-
const hashed = PROJECT_BADGE_COLORS[hashProjectName(displayName)];
|
|
1845
|
-
assert(hashed !== void 0, "hash always returns valid index");
|
|
1846
|
-
return hashed;
|
|
1847
|
-
}
|
|
1848
|
-
function identity(text) {
|
|
1849
|
-
return text;
|
|
1850
|
-
}
|
|
1851
|
-
function createStyles(useColor) {
|
|
1852
|
-
if (!useColor) return {
|
|
1853
|
-
diff: {
|
|
1854
|
-
expected: identity,
|
|
1855
|
-
received: identity
|
|
1856
|
-
},
|
|
1857
|
-
dim: identity,
|
|
1858
|
-
failBadge: identity,
|
|
1859
|
-
lineNumber: identity,
|
|
1860
|
-
location: identity,
|
|
1861
|
-
path: {
|
|
1862
|
-
dir: identity,
|
|
1863
|
-
file: identity
|
|
1864
|
-
},
|
|
1865
|
-
runBadge: identity,
|
|
1866
|
-
status: {
|
|
1867
|
-
fail: identity,
|
|
1868
|
-
pass: identity,
|
|
1869
|
-
pending: identity
|
|
1870
|
-
},
|
|
1871
|
-
summary: {
|
|
1872
|
-
failed: identity,
|
|
1873
|
-
passed: identity,
|
|
1874
|
-
pending: identity
|
|
1875
|
-
}
|
|
1876
|
-
};
|
|
1877
|
-
return {
|
|
1878
|
-
diff: {
|
|
1879
|
-
expected: color.green,
|
|
1880
|
-
received: color.red
|
|
1881
|
-
},
|
|
1882
|
-
dim: color.dim,
|
|
1883
|
-
failBadge: (text) => color.bgRed(color.white(color.bold(text))),
|
|
1884
|
-
lineNumber: color.gray,
|
|
1885
|
-
location: color.cyan,
|
|
1886
|
-
path: {
|
|
1887
|
-
dir: color.dim,
|
|
1888
|
-
file: color.bold
|
|
1889
|
-
},
|
|
1890
|
-
runBadge: (text) => color.bgCyan(color.black(color.bold(text))),
|
|
1891
|
-
status: {
|
|
1892
|
-
fail: color.red,
|
|
1893
|
-
pass: color.green,
|
|
1894
|
-
pending: color.yellow
|
|
1895
|
-
},
|
|
1896
|
-
summary: {
|
|
1897
|
-
failed: (text) => color.bold(color.red(text)),
|
|
1898
|
-
passed: (text) => color.bold(color.green(text)),
|
|
1899
|
-
pending: (text) => color.bold(color.yellow(text))
|
|
1900
|
-
}
|
|
1901
|
-
};
|
|
1902
|
-
}
|
|
1903
|
-
function sumFileDuration(file) {
|
|
1904
|
-
let total = 0;
|
|
1905
|
-
for (const test of file.testResults) if (test.duration !== void 0) total += test.duration;
|
|
1906
|
-
return total;
|
|
1907
|
-
}
|
|
1908
|
-
function computeProjectStats(result) {
|
|
1909
|
-
let durationMs = 0;
|
|
1910
|
-
let failedFiles = 0;
|
|
1911
|
-
let passedFiles = 0;
|
|
1912
|
-
let skippedFiles = 0;
|
|
1913
|
-
for (const file of result.testResults) {
|
|
1914
|
-
if (file.numFailingTests > 0 || hasExecError(file)) failedFiles++;
|
|
1915
|
-
else if (file.numPassingTests === 0 && file.numPendingTests > 0) skippedFiles++;
|
|
1916
|
-
else passedFiles++;
|
|
1917
|
-
durationMs += sumFileDuration(file);
|
|
1918
|
-
}
|
|
1919
|
-
return {
|
|
1920
|
-
durationMs,
|
|
1921
|
-
failedFiles,
|
|
1922
|
-
passedFiles,
|
|
1923
|
-
skippedFiles,
|
|
1924
|
-
totalTests: result.numTotalTests
|
|
1925
|
-
};
|
|
1926
|
-
}
|
|
1927
|
-
function formatFileFailures(file, options, styles, failureCtx) {
|
|
1928
|
-
const lines = [];
|
|
1929
|
-
const displayPath = resolveDisplayPath(file.testFilePath, options.sourceMapper);
|
|
1930
|
-
for (const testCase of file.testResults) if (testCase.status === "failed") {
|
|
1931
|
-
const index = failureCtx.currentIndex;
|
|
1932
|
-
failureCtx.currentIndex++;
|
|
1933
|
-
lines.push(formatFailure({
|
|
1934
|
-
failureIndex: index,
|
|
1935
|
-
filePath: displayPath,
|
|
1936
|
-
showLuau: options.showLuau,
|
|
1937
|
-
sourceMapper: options.sourceMapper,
|
|
1938
|
-
styles,
|
|
1939
|
-
test: testCase,
|
|
1940
|
-
totalFailures: failureCtx.totalFailures,
|
|
1941
|
-
useColor: options.color
|
|
1942
|
-
}));
|
|
1943
|
-
}
|
|
1944
|
-
return lines.join("\n");
|
|
1945
|
-
}
|
|
1946
|
-
function getTerminalWidth() {
|
|
1947
|
-
return "columns" in process.stdout && process.stdout.columns || 80;
|
|
1948
|
-
}
|
|
1949
|
-
function formatExecErrorDetail(file, styles, failureCtx, sourceMapper) {
|
|
1950
|
-
const lines = [];
|
|
1951
|
-
const index = failureCtx.currentIndex;
|
|
1952
|
-
failureCtx.currentIndex++;
|
|
1953
|
-
assert(file.failureMessage !== void 0, "exec error files have failureMessage");
|
|
1954
|
-
const displayPath = resolveDisplayPath(file.testFilePath, sourceMapper);
|
|
1955
|
-
const errorMessage = cleanExecErrorMessage(file.failureMessage);
|
|
1956
|
-
const counter = `[${index}/${failureCtx.totalFailures}]`;
|
|
1957
|
-
const termWidth = getTerminalWidth();
|
|
1958
|
-
const fillWidth = Math.max(1, termWidth - counter.length - 3);
|
|
1959
|
-
const separator = styles.dim(styles.status.fail(`${"⎯".repeat(fillWidth)}${counter}\u23af`));
|
|
1960
|
-
lines.push(` ${styles.failBadge(" FAIL ")} ${styles.status.fail(displayPath)}`, ` ${styles.status.fail("Test suite failed to run")}`, "", ` ${styles.status.fail(errorMessage)}`);
|
|
1961
|
-
const hint = getExecErrorHint(errorMessage);
|
|
1962
|
-
if (hint !== void 0) lines.push("", ` ${styles.dim("Hint:")} ${hint}`);
|
|
1963
|
-
lines.push("", ` ${separator}`);
|
|
1964
|
-
return lines.join("\n");
|
|
1965
|
-
}
|
|
1966
|
-
function formatTestInGroup(testCase, styles) {
|
|
1967
|
-
const duration = testCase.duration !== void 0 ? styles.lineNumber(` ${testCase.duration}ms`) : "";
|
|
1968
|
-
if (testCase.status === "passed") return `${styles.status.pass(" ✓")}${styles.status.fail(` ${testCase.title}`)}${duration}`;
|
|
1969
|
-
const failedText = ` × ${testCase.title}`;
|
|
1970
|
-
return `${styles.status.fail(failedText)}${duration}`;
|
|
1971
|
-
}
|
|
1972
|
-
function formatDescribeGroup(describeName, tests, styles) {
|
|
1973
|
-
const lines = [];
|
|
1974
|
-
const groupHasFailure = tests.some((testCase) => testCase.status === "failed");
|
|
1975
|
-
const groupTestCount = tests.length;
|
|
1976
|
-
const groupDuration = tests.reduce((sum, testCase) => sum + (testCase.duration ?? 0), 0);
|
|
1977
|
-
const groupDurationStr = styles.lineNumber(` ${groupDuration}ms`);
|
|
1978
|
-
if (groupHasFailure) {
|
|
1979
|
-
const failedCount = tests.filter((testCase) => testCase.status === "failed").length;
|
|
1980
|
-
const groupMeta = styles.dim(`(${groupTestCount} tests | `) + styles.summary.failed(`${failedCount} failed`) + styles.dim(")");
|
|
1981
|
-
const header = styles.status.fail(` ❯ ${describeName}`);
|
|
1982
|
-
lines.push(`${header} ${groupMeta}${groupDurationStr}`);
|
|
1983
|
-
for (const testCase of tests) lines.push(formatTestInGroup(testCase, styles));
|
|
1984
|
-
} else {
|
|
1985
|
-
const groupMeta = styles.dim(`(${groupTestCount} tests)`);
|
|
1986
|
-
const marker = styles.status.pass(" ✓");
|
|
1987
|
-
const name = styles.status.fail(` ${describeName}`);
|
|
1988
|
-
lines.push(`${marker}${name} ${groupMeta}${groupDurationStr}`);
|
|
1989
|
-
}
|
|
1990
|
-
return lines;
|
|
1991
|
-
}
|
|
1992
|
-
function groupByDescribe(tests) {
|
|
1993
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1994
|
-
for (const test of tests) {
|
|
1995
|
-
const describeName = test.ancestorTitles[0] ?? "(root)";
|
|
1996
|
-
const group = groups.get(describeName);
|
|
1997
|
-
if (group !== void 0) group.push(test);
|
|
1998
|
-
else groups.set(describeName, [test]);
|
|
1999
|
-
}
|
|
2000
|
-
return groups;
|
|
2001
|
-
}
|
|
2002
|
-
function formatFailedFileSummary(file, testCount, styles, displayPath) {
|
|
2003
|
-
const lines = [];
|
|
2004
|
-
const failedMeta = styles.summary.failed(`${file.numFailingTests} failed`);
|
|
2005
|
-
const meta = `${styles.dim(`(${testCount} tests | `)}${failedMeta}${styles.dim(")")}`;
|
|
2006
|
-
const header = styles.status.fail(` ❯ ${displayPath}`);
|
|
2007
|
-
lines.push(`${header} ${meta}`);
|
|
2008
|
-
const groups = groupByDescribe(file.testResults);
|
|
2009
|
-
for (const [describeName, tests] of groups) lines.push(...formatDescribeGroup(describeName, tests, styles));
|
|
2010
|
-
return lines;
|
|
2011
|
-
}
|
|
2012
|
-
function formatFilePath(filePath, styles) {
|
|
2013
|
-
const directory = path.dirname(filePath);
|
|
2014
|
-
const base = path.basename(filePath);
|
|
2015
|
-
const directoryWithSlash = styles.path.dir(`${directory}/`);
|
|
2016
|
-
const fileName = styles.path.file(base);
|
|
2017
|
-
return directory && directory !== "." ? directoryWithSlash + fileName : fileName;
|
|
2018
|
-
}
|
|
2019
|
-
function formatExecErrorFileSummary(file, formattedPath, styles) {
|
|
2020
|
-
const symbol = styles.status.fail("✗");
|
|
2021
|
-
assert(file.failureMessage !== void 0, "exec error files have failureMessage");
|
|
2022
|
-
const errorMessage = cleanExecErrorMessage(file.failureMessage);
|
|
2023
|
-
return [` ${symbol} ${formattedPath}`, ` ${styles.status.fail(errorMessage)}`];
|
|
2024
|
-
}
|
|
2025
|
-
function formatPass(test, styles) {
|
|
2026
|
-
const duration = test.duration !== void 0 ? styles.dim(` ${test.duration}ms`) : "";
|
|
2027
|
-
return styles.status.pass(` ✓ ${test.fullName}`) + duration;
|
|
2028
|
-
}
|
|
2029
|
-
function formatPassedFileSummary(file, ctx) {
|
|
2030
|
-
const lines = [];
|
|
2031
|
-
const fileMs = sumFileDuration(file);
|
|
2032
|
-
const symbol = ctx.styles.status.pass("✓");
|
|
2033
|
-
const duration = fileMs > 0 ? ` - ${fileMs}ms` : "";
|
|
2034
|
-
const meta = ctx.styles.dim(`(${ctx.testCount} tests${duration})`);
|
|
2035
|
-
lines.push(` ${symbol} ${ctx.formattedPath} ${meta}`);
|
|
2036
|
-
if (ctx.verbose) {
|
|
2037
|
-
for (const testCase of file.testResults) if (testCase.status === "passed") lines.push(formatPass(testCase, ctx.styles));
|
|
2038
|
-
}
|
|
2039
|
-
return lines;
|
|
2040
|
-
}
|
|
2041
|
-
function formatFileSummary(file, options, styles) {
|
|
2042
|
-
const displayPath = resolveDisplayPath(file.testFilePath, options.sourceMapper);
|
|
2043
|
-
const formattedPath = formatFilePath(displayPath, styles);
|
|
2044
|
-
const testCount = file.numPassingTests + file.numFailingTests + file.numPendingTests;
|
|
2045
|
-
if (file.numFailingTests > 0) return formatFailedFileSummary(file, testCount, styles, displayPath).join("\n");
|
|
2046
|
-
if (hasExecError(file)) return formatExecErrorFileSummary(file, formattedPath, styles).join("\n");
|
|
2047
|
-
if (file.numPassingTests === 0 && file.numPendingTests > 0) return ` ${styles.status.pending("↓")} ${formattedPath} ${styles.dim(`(${testCount} tests)`)}`;
|
|
2048
|
-
return formatPassedFileSummary(file, {
|
|
2049
|
-
formattedPath,
|
|
2050
|
-
styles,
|
|
2051
|
-
testCount,
|
|
2052
|
-
verbose: options.verbose
|
|
2053
|
-
}).join("\n");
|
|
2054
|
-
}
|
|
2055
|
-
function formatLogHints(options, styles) {
|
|
2056
|
-
const lines = [];
|
|
2057
|
-
if (options.outputFile !== void 0) lines.push(styles.dim(` View ${options.outputFile} for full Jest output`));
|
|
2058
|
-
if (options.gameOutput !== void 0) lines.push(styles.dim(` View ${options.gameOutput} for Roblox game logs`));
|
|
2059
|
-
return lines.join("\n");
|
|
2060
|
-
}
|
|
2061
|
-
function mergeJestResults(results) {
|
|
2062
|
-
let numberFailedTests = 0;
|
|
2063
|
-
let numberPassedTests = 0;
|
|
2064
|
-
let numberPendingTests = 0;
|
|
2065
|
-
let numberTodoTests = 0;
|
|
2066
|
-
let numberTotalTests = 0;
|
|
2067
|
-
let startTime = Number.POSITIVE_INFINITY;
|
|
2068
|
-
let success = true;
|
|
2069
|
-
let snapshotAdded = 0;
|
|
2070
|
-
let snapshotMatched = 0;
|
|
2071
|
-
let snapshotTotal = 0;
|
|
2072
|
-
let snapshotUnmatched = 0;
|
|
2073
|
-
let snapshotUpdated = 0;
|
|
2074
|
-
let hasSnapshot = false;
|
|
2075
|
-
const testResults = [];
|
|
2076
|
-
for (const result of results) {
|
|
2077
|
-
numberFailedTests += result.numFailedTests;
|
|
2078
|
-
numberPassedTests += result.numPassedTests;
|
|
2079
|
-
numberPendingTests += result.numPendingTests;
|
|
2080
|
-
numberTodoTests += result.numTodoTests ?? 0;
|
|
2081
|
-
numberTotalTests += result.numTotalTests;
|
|
2082
|
-
startTime = Math.min(startTime, result.startTime);
|
|
2083
|
-
success &&= result.success;
|
|
2084
|
-
testResults.push(...result.testResults);
|
|
2085
|
-
if (result.snapshot !== void 0) {
|
|
2086
|
-
hasSnapshot = true;
|
|
2087
|
-
snapshotAdded += result.snapshot.added;
|
|
2088
|
-
snapshotMatched += result.snapshot.matched;
|
|
2089
|
-
snapshotTotal += result.snapshot.total;
|
|
2090
|
-
snapshotUnmatched += result.snapshot.unmatched;
|
|
2091
|
-
snapshotUpdated += result.snapshot.updated;
|
|
2092
|
-
}
|
|
2093
|
-
}
|
|
2094
|
-
return {
|
|
2095
|
-
numFailedTests: numberFailedTests,
|
|
2096
|
-
numPassedTests: numberPassedTests,
|
|
2097
|
-
numPendingTests: numberPendingTests,
|
|
2098
|
-
numTodoTests: numberTodoTests > 0 ? numberTodoTests : void 0,
|
|
2099
|
-
numTotalTests: numberTotalTests,
|
|
2100
|
-
snapshot: hasSnapshot ? {
|
|
2101
|
-
added: snapshotAdded,
|
|
2102
|
-
matched: snapshotMatched,
|
|
2103
|
-
total: snapshotTotal,
|
|
2104
|
-
unmatched: snapshotUnmatched,
|
|
2105
|
-
updated: snapshotUpdated
|
|
2106
|
-
} : void 0,
|
|
2107
|
-
startTime,
|
|
2108
|
-
success,
|
|
2109
|
-
testResults
|
|
2110
|
-
};
|
|
2111
|
-
}
|
|
2112
|
-
function expandTabs(text, tabWidth = 4) {
|
|
2113
|
-
let result = "";
|
|
2114
|
-
for (const char of text) if (char === " ") {
|
|
2115
|
-
const spaces = tabWidth - result.length % tabWidth;
|
|
2116
|
-
result += " ".repeat(spaces);
|
|
2117
|
-
} else result += char;
|
|
2118
|
-
return result;
|
|
2119
|
-
}
|
|
2120
|
-
function highlightSyntax(filePath, code, useColor) {
|
|
2121
|
-
if (!useColor) return code;
|
|
2122
|
-
return highlightCode(filePath, code);
|
|
2123
|
-
}
|
|
2124
|
-
function formatDiffBlock(parsed, styles) {
|
|
2125
|
-
if (parsed.snapshotDiff !== void 0) {
|
|
2126
|
-
const lines = [""];
|
|
2127
|
-
for (const diffLine of parsed.snapshotDiff.split("\n")) if (diffLine.startsWith("- ")) lines.push(styles.diff.expected(diffLine));
|
|
2128
|
-
else if (diffLine.startsWith("+ ")) lines.push(styles.diff.received(diffLine));
|
|
2129
|
-
else lines.push(styles.dim(diffLine));
|
|
2130
|
-
return lines;
|
|
2131
|
-
}
|
|
2132
|
-
if (parsed.expected !== void 0 && parsed.received !== void 0) return [
|
|
2133
|
-
"",
|
|
2134
|
-
styles.diff.expected("- Expected"),
|
|
2135
|
-
styles.diff.received("+ Received"),
|
|
2136
|
-
"",
|
|
2137
|
-
styles.diff.expected(`- ${parsed.expected}`),
|
|
2138
|
-
styles.diff.received(`+ ${parsed.received}`)
|
|
2139
|
-
];
|
|
2140
|
-
return [];
|
|
2141
|
-
}
|
|
2142
|
-
function formatErrorLine(parsed, styles, useColor) {
|
|
2143
|
-
if (useColor && parsed.message.startsWith("Error:")) return styles.status.fail(color.bold("Error:") + parsed.message.slice(6));
|
|
2144
|
-
return styles.status.fail(parsed.message);
|
|
2145
|
-
}
|
|
2146
|
-
function formatFallbackSnippet(message, styles, useColor) {
|
|
2147
|
-
const location = parseSourceLocation(message);
|
|
2148
|
-
if (location === void 0) return [];
|
|
2149
|
-
const snippet = getSourceSnippet({
|
|
2150
|
-
column: location.column,
|
|
2151
|
-
context: 2,
|
|
2152
|
-
filePath: location.path,
|
|
2153
|
-
line: location.line
|
|
2154
|
-
});
|
|
2155
|
-
if (snippet === void 0) return [];
|
|
2156
|
-
return ["", formatSourceSnippet(snippet, location.path, {
|
|
2157
|
-
styles,
|
|
2158
|
-
useColor
|
|
2159
|
-
})];
|
|
2160
|
-
}
|
|
2161
|
-
function formatMappedLocationSnippets(loc, showLuau, styles, useColor) {
|
|
2162
|
-
const snippets = [];
|
|
2163
|
-
if (loc.tsPath !== void 0 && loc.tsLine !== void 0) {
|
|
2164
|
-
const tsSnippet = getSourceSnippet({
|
|
2165
|
-
column: loc.tsColumn,
|
|
2166
|
-
context: 2,
|
|
2167
|
-
filePath: loc.tsPath,
|
|
2168
|
-
line: loc.tsLine,
|
|
2169
|
-
sourceContent: loc.sourceContent
|
|
2170
|
-
});
|
|
2171
|
-
if (tsSnippet !== void 0) {
|
|
2172
|
-
const label = showLuau ? "TypeScript" : void 0;
|
|
2173
|
-
snippets.push("", formatSourceSnippet(tsSnippet, loc.tsPath, {
|
|
2174
|
-
language: label,
|
|
2175
|
-
styles,
|
|
2176
|
-
useColor
|
|
2177
|
-
}));
|
|
2178
|
-
}
|
|
2179
|
-
if (showLuau) {
|
|
2180
|
-
const luauSnippet = getSourceSnippet({
|
|
2181
|
-
context: 2,
|
|
2182
|
-
filePath: loc.luauPath,
|
|
2183
|
-
line: loc.luauLine
|
|
2184
|
-
});
|
|
2185
|
-
if (luauSnippet !== void 0) snippets.push("", formatSourceSnippet(luauSnippet, loc.luauPath, {
|
|
2186
|
-
language: "Luau",
|
|
2187
|
-
styles,
|
|
2188
|
-
useColor
|
|
2189
|
-
}));
|
|
2190
|
-
}
|
|
2191
|
-
} else {
|
|
2192
|
-
const luauSnippet = getSourceSnippet({
|
|
2193
|
-
context: 2,
|
|
2194
|
-
filePath: loc.luauPath,
|
|
2195
|
-
line: loc.luauLine
|
|
2196
|
-
});
|
|
2197
|
-
if (luauSnippet !== void 0) snippets.push("", formatSourceSnippet(luauSnippet, loc.luauPath, {
|
|
2198
|
-
styles,
|
|
2199
|
-
useColor
|
|
2200
|
-
}));
|
|
2201
|
-
}
|
|
2202
|
-
return snippets;
|
|
2203
|
-
}
|
|
2204
|
-
function formatSnapshotCallSnippet(filePath, styles, useColor) {
|
|
2205
|
-
if (!fs$1.existsSync(filePath)) return [];
|
|
2206
|
-
const content = fs$1.readFileSync(filePath, "utf-8");
|
|
2207
|
-
const snapshotIndices = content.split("\n").reduce((accumulator, fileLine, index) => {
|
|
2208
|
-
if (fileLine.includes("toMatchSnapshot")) accumulator.push(index);
|
|
2209
|
-
return accumulator;
|
|
2210
|
-
}, []);
|
|
2211
|
-
if (snapshotIndices.length !== 1) return [];
|
|
2212
|
-
const snippet = getSourceSnippet({
|
|
2213
|
-
context: 2,
|
|
2214
|
-
filePath,
|
|
2215
|
-
line: snapshotIndices[0] + 1,
|
|
2216
|
-
sourceContent: content
|
|
2217
|
-
});
|
|
2218
|
-
if (snippet === void 0) return [];
|
|
2219
|
-
return ["", formatSourceSnippet(snippet, filePath, {
|
|
2220
|
-
styles,
|
|
2221
|
-
useColor
|
|
2222
|
-
})];
|
|
2223
|
-
}
|
|
2224
|
-
function resolveSourceSnippets(options) {
|
|
2225
|
-
const { filePath, hasSnapshotDiff, mappedLocations, message, showLuau, sourceMapper, styles, useColor } = options;
|
|
2226
|
-
if (mappedLocations.length > 0) return mappedLocations.flatMap((loc) => {
|
|
2227
|
-
return formatMappedLocationSnippets(loc, showLuau, styles, useColor);
|
|
2228
|
-
});
|
|
2229
|
-
const fallback = formatFallbackSnippet(message, styles, useColor);
|
|
2230
|
-
if (fallback.length > 0) return fallback;
|
|
2231
|
-
if (hasSnapshotDiff && filePath !== void 0) return formatSnapshotCallSnippet(resolveDisplayPath(filePath, sourceMapper), styles, useColor);
|
|
2232
|
-
return [];
|
|
2233
|
-
}
|
|
2234
|
-
function formatFailureMessage(originalMessage, options) {
|
|
2235
|
-
const { filePath, showLuau, sourceMapper, styles, useColor } = options;
|
|
2236
|
-
let mappedLocations = [];
|
|
2237
|
-
let message = originalMessage;
|
|
2238
|
-
if (sourceMapper !== void 0) ({locations: mappedLocations, message} = sourceMapper.mapFailureWithLocations(originalMessage));
|
|
2239
|
-
const parsed = parseErrorMessage(originalMessage);
|
|
2240
|
-
return [
|
|
2241
|
-
formatErrorLine(parsed, styles, useColor),
|
|
2242
|
-
...formatDiffBlock(parsed, styles),
|
|
2243
|
-
...resolveSourceSnippets({
|
|
2244
|
-
filePath,
|
|
2245
|
-
hasSnapshotDiff: parsed.snapshotDiff !== void 0,
|
|
2246
|
-
mappedLocations,
|
|
2247
|
-
message,
|
|
2248
|
-
showLuau,
|
|
2249
|
-
sourceMapper,
|
|
2250
|
-
styles,
|
|
2251
|
-
useColor
|
|
2252
|
-
})
|
|
2253
|
-
];
|
|
2254
|
-
}
|
|
2255
|
-
function formatSnapshotLine(snapshot, styles) {
|
|
2256
|
-
if (snapshot === void 0 || snapshot.unmatched === 0) return;
|
|
2257
|
-
return `${styles.dim(" Snapshots")} ${styles.summary.failed(`${snapshot.unmatched} failed`)}`;
|
|
2258
|
-
}
|
|
2259
|
-
//#endregion
|
|
2260
|
-
//#region src/formatters/agent.ts
|
|
2261
|
-
function formatAgent(result, options) {
|
|
2262
|
-
const lines = [];
|
|
2263
|
-
const execErrors = result.testResults.filter(hasExecError);
|
|
2264
|
-
if (result.numFailedTests > 0 || execErrors.length > 0) {
|
|
2265
|
-
lines.push(...formatFileHeaders(result, options), "");
|
|
2266
|
-
const totalFailures = result.numFailedTests + execErrors.length;
|
|
2267
|
-
lines.push(`${"⎯".repeat(3)} Failed Tests ${totalFailures} ${"⎯".repeat(3)}`, "");
|
|
2268
|
-
if (result.numFailedTests > 0) {
|
|
2269
|
-
const failureLines = formatFailures(result, result.numFailedTests, options);
|
|
2270
|
-
lines.push(...failureLines);
|
|
2271
|
-
}
|
|
2272
|
-
for (const file of execErrors) lines.push(...formatExecError(file, options));
|
|
2273
|
-
const hints = formatAgentLogHints(options);
|
|
2274
|
-
if (hints !== "") lines.push(hints);
|
|
2275
|
-
}
|
|
2276
|
-
lines.push(...formatSummarySection(result, options));
|
|
2277
|
-
return lines.join("\n");
|
|
2278
|
-
}
|
|
2279
|
-
function formatAgentMultiProject(projects, options) {
|
|
2280
|
-
const lines = [];
|
|
2281
|
-
for (const { displayName, result } of projects) lines.push(...formatAgentProjectHeader(displayName, result, options));
|
|
2282
|
-
const stats = collectMultiProjectStats(projects);
|
|
2283
|
-
if (stats.totalFailed + stats.allExecErrors.length > 0) lines.push(...formatMultiProjectFailures(projects, stats, options));
|
|
2284
|
-
lines.push(...formatMultiProjectSummary(stats, options));
|
|
2285
|
-
return lines.join("\n");
|
|
2286
|
-
}
|
|
2287
|
-
function formatTypeErrorLabel(count) {
|
|
2288
|
-
if (count === 0) return "no errors";
|
|
2289
|
-
return `${count} error${count === 1 ? "" : "s"}`;
|
|
2290
|
-
}
|
|
2291
|
-
function formatSummarySection(result, options) {
|
|
2292
|
-
const lines = [];
|
|
2293
|
-
const failedFiles = result.testResults.filter((file) => file.numFailingTests > 0 || hasExecError(file)).length;
|
|
2294
|
-
const passedFiles = result.testResults.filter((file) => file.numFailingTests === 0 && !hasExecError(file)).length;
|
|
2295
|
-
const totalFiles = failedFiles + passedFiles;
|
|
2296
|
-
const fileParts = [];
|
|
2297
|
-
if (failedFiles > 0) fileParts.push(`${failedFiles} failed`);
|
|
2298
|
-
if (passedFiles > 0) fileParts.push(`${passedFiles} passed`);
|
|
2299
|
-
lines.push(` Test Files ${fileParts.join(" | ")} (${totalFiles})`);
|
|
2300
|
-
const testParts = [];
|
|
2301
|
-
if (result.numFailedTests > 0) testParts.push(`${result.numFailedTests} failed`);
|
|
2302
|
-
if (result.numPassedTests > 0) testParts.push(`${result.numPassedTests} passed`);
|
|
2303
|
-
if (result.numPendingTests > 0) testParts.push(`${result.numPendingTests} skipped`);
|
|
2304
|
-
const totalTests = result.numTotalTests;
|
|
2305
|
-
lines.push(` Tests ${testParts.join(" | ")} (${totalTests})`);
|
|
2306
|
-
if (options.typeErrorCount !== void 0) {
|
|
2307
|
-
const typeLabel = formatTypeErrorLabel(options.typeErrorCount);
|
|
2308
|
-
lines.push(`Type Errors ${typeLabel}`);
|
|
2309
|
-
}
|
|
2310
|
-
return lines;
|
|
2311
|
-
}
|
|
2312
|
-
function makeRelative$1(filePath, rootDirectory) {
|
|
2313
|
-
const normalizedPath = filePath.replaceAll("\\", "/");
|
|
2314
|
-
const normalizedRoot = rootDirectory.replaceAll("\\", "/");
|
|
2315
|
-
if (normalizedPath.startsWith(normalizedRoot)) return path.relative(normalizedRoot, normalizedPath).replaceAll("\\", "/");
|
|
2316
|
-
return filePath;
|
|
2317
|
-
}
|
|
2318
|
-
function formatFileHeaderExecError(file, options) {
|
|
2319
|
-
return [` ❯ ${makeRelative$1(resolveDisplayPath(file.testFilePath, options.sourceMapper), options.rootDir)} (suite failed to run)`];
|
|
2320
|
-
}
|
|
2321
|
-
function formatFileHeaderFailures(file, options) {
|
|
2322
|
-
const lines = [];
|
|
2323
|
-
const relativePath = makeRelative$1(resolveDisplayPath(file.testFilePath, options.sourceMapper), options.rootDir);
|
|
2324
|
-
const totalTests = file.numFailingTests + file.numPassingTests + file.numPendingTests;
|
|
2325
|
-
const testWord = totalTests === 1 ? "test" : "tests";
|
|
2326
|
-
lines.push(` ❯ ${relativePath} (${totalTests} ${testWord} | ${file.numFailingTests} failed)`);
|
|
2327
|
-
for (const test of file.testResults) if (test.status === "failed") {
|
|
2328
|
-
const duration = test.duration !== void 0 ? ` ${String(test.duration)}ms` : "";
|
|
2329
|
-
lines.push(` × ${test.title}${duration}`);
|
|
2330
|
-
}
|
|
2331
|
-
return lines;
|
|
2332
|
-
}
|
|
2333
|
-
function formatFileHeaders(result, options) {
|
|
2334
|
-
const lines = [];
|
|
2335
|
-
for (const file of result.testResults) {
|
|
2336
|
-
if (hasExecError(file)) {
|
|
2337
|
-
lines.push(...formatFileHeaderExecError(file, options));
|
|
2338
|
-
continue;
|
|
2339
|
-
}
|
|
2340
|
-
if (file.numFailingTests === 0) continue;
|
|
2341
|
-
lines.push(...formatFileHeaderFailures(file, options));
|
|
2342
|
-
}
|
|
2343
|
-
return lines;
|
|
2344
|
-
}
|
|
2345
|
-
function formatExecError(file, options) {
|
|
2346
|
-
const lines = [];
|
|
2347
|
-
const relativePath = makeRelative$1(resolveDisplayPath(file.testFilePath, options.sourceMapper), options.rootDir);
|
|
2348
|
-
assert(file.failureMessage !== void 0, "exec error files have failureMessage");
|
|
2349
|
-
const errorMessage = cleanExecErrorMessage(file.failureMessage);
|
|
2350
|
-
lines.push(` FAIL ${relativePath}`, errorMessage);
|
|
2351
|
-
const hint = getExecErrorHint(errorMessage);
|
|
2352
|
-
if (hint !== void 0) lines.push(`Hint: ${hint}`);
|
|
2353
|
-
lines.push("");
|
|
2354
|
-
return lines;
|
|
2355
|
-
}
|
|
2356
|
-
function formatSize(bytes) {
|
|
2357
|
-
if (bytes < 1024) return `${bytes}b`;
|
|
2358
|
-
return `${Math.round(bytes / 1024)}kb`;
|
|
2359
|
-
}
|
|
2360
|
-
function formatAgentLogHints(options) {
|
|
2361
|
-
const lines = [];
|
|
2362
|
-
if (options.outputFile !== void 0) {
|
|
2363
|
-
const size = options.outputFileSize !== void 0 ? ` (${formatSize(options.outputFileSize)})` : "";
|
|
2364
|
-
lines.push(`View ${options.outputFile} for full Jest output${size}`);
|
|
2365
|
-
}
|
|
2366
|
-
if (options.gameOutput !== void 0) {
|
|
2367
|
-
const size = options.gameOutputSize !== void 0 ? ` (${formatSize(options.gameOutputSize)})` : "";
|
|
2368
|
-
lines.push(`View ${options.gameOutput} for Roblox game logs${size}`);
|
|
2369
|
-
}
|
|
2370
|
-
return lines.join("\n");
|
|
2371
|
-
}
|
|
2372
|
-
function collectFailedTests(result, sourceMapper) {
|
|
2373
|
-
const failures = [];
|
|
2374
|
-
for (const file of result.testResults) {
|
|
2375
|
-
const displayPath = resolveDisplayPath(file.testFilePath, sourceMapper);
|
|
2376
|
-
for (const test of file.testResults) if (test.status === "failed") failures.push({
|
|
2377
|
-
filePath: displayPath,
|
|
2378
|
-
test
|
|
2379
|
-
});
|
|
2380
|
-
}
|
|
2381
|
-
return failures;
|
|
2382
|
-
}
|
|
2383
|
-
function getSnippetLevel(totalFailures) {
|
|
2384
|
-
if (totalFailures <= 2) return "both";
|
|
2385
|
-
if (totalFailures <= 5) return "ts-only";
|
|
2386
|
-
return "none";
|
|
2387
|
-
}
|
|
2388
|
-
function findFailureLocation(mappedLocations, message) {
|
|
2389
|
-
if (mappedLocations.length > 0) {
|
|
2390
|
-
const loc = mappedLocations[0];
|
|
2391
|
-
assert(loc !== void 0, "array with length > 0 has element 0");
|
|
2392
|
-
if (loc.tsPath !== void 0 && loc.tsLine !== void 0) return {
|
|
2393
|
-
line: loc.tsLine,
|
|
2394
|
-
path: loc.tsPath
|
|
2395
|
-
};
|
|
2396
|
-
return {
|
|
2397
|
-
line: loc.luauLine,
|
|
2398
|
-
path: loc.luauPath
|
|
2399
|
-
};
|
|
2400
|
-
}
|
|
2401
|
-
return parseSourceLocation(message);
|
|
2402
|
-
}
|
|
2403
|
-
function formatSnippetBlock(snippetResult) {
|
|
2404
|
-
if (snippetResult === void 0) return;
|
|
2405
|
-
const lines = [];
|
|
2406
|
-
for (const line of snippetResult.lines) {
|
|
2407
|
-
const prefix = line.num === snippetResult.failureLine ? ">" : " ";
|
|
2408
|
-
lines.push(`${prefix} ${line.num}| ${line.content}`);
|
|
2409
|
-
}
|
|
2410
|
-
return lines.join("\n");
|
|
2411
|
-
}
|
|
2412
|
-
function getTsSnippets(loc, snippetLevel, rootDirectory) {
|
|
2413
|
-
assert(loc.tsPath !== void 0 && loc.tsLine !== void 0, "caller checked ts fields");
|
|
2414
|
-
const result = [];
|
|
2415
|
-
const tsSnippet = formatSnippetBlock(getSourceSnippet({
|
|
2416
|
-
column: loc.tsColumn,
|
|
2417
|
-
context: 1,
|
|
2418
|
-
filePath: loc.tsPath,
|
|
2419
|
-
line: loc.tsLine,
|
|
2420
|
-
sourceContent: loc.sourceContent
|
|
2421
|
-
}));
|
|
2422
|
-
if (tsSnippet !== void 0) {
|
|
2423
|
-
const relativeTsPath = makeRelative$1(loc.tsPath, rootDirectory);
|
|
2424
|
-
const label = snippetLevel === "both" ? `TS ${relativeTsPath}:${loc.tsLine}\n` : "";
|
|
2425
|
-
result.push(`${label}${tsSnippet}`);
|
|
2426
|
-
}
|
|
2427
|
-
if (snippetLevel === "both") {
|
|
2428
|
-
const luauSnippet = formatSnippetBlock(getSourceSnippet({
|
|
2429
|
-
context: 1,
|
|
2430
|
-
filePath: loc.luauPath,
|
|
2431
|
-
line: loc.luauLine
|
|
2432
|
-
}));
|
|
2433
|
-
if (luauSnippet !== void 0) {
|
|
2434
|
-
const relativeLuauPath = makeRelative$1(loc.luauPath, rootDirectory);
|
|
2435
|
-
result.push(`Luau ${relativeLuauPath}:${loc.luauLine}\n${luauSnippet}`);
|
|
2436
|
-
}
|
|
2437
|
-
}
|
|
2438
|
-
return result;
|
|
2439
|
-
}
|
|
2440
|
-
function getLuauOnlySnippet(loc) {
|
|
2441
|
-
const snippet = formatSnippetBlock(getSourceSnippet({
|
|
2442
|
-
context: 1,
|
|
2443
|
-
filePath: loc.luauPath,
|
|
2444
|
-
line: loc.luauLine
|
|
2445
|
-
}));
|
|
2446
|
-
return snippet !== void 0 ? [snippet] : [];
|
|
2447
|
-
}
|
|
2448
|
-
function getMappedSnippets(loc, snippetLevel, rootDirectory) {
|
|
2449
|
-
if (loc.tsPath !== void 0 && loc.tsLine !== void 0) return getTsSnippets(loc, snippetLevel, rootDirectory);
|
|
2450
|
-
return getLuauOnlySnippet(loc);
|
|
2451
|
-
}
|
|
2452
|
-
function getFallbackSnippet(location) {
|
|
2453
|
-
const snippet = formatSnippetBlock(getSourceSnippet({
|
|
2454
|
-
context: 1,
|
|
2455
|
-
filePath: location.path,
|
|
2456
|
-
line: location.line
|
|
2457
|
-
}));
|
|
2458
|
-
return snippet !== void 0 ? [snippet] : [];
|
|
2459
|
-
}
|
|
2460
|
-
function getFailureSnippets(mappedLocations, location, snippetLevel, rootDirectory) {
|
|
2461
|
-
if (snippetLevel === "none") return [];
|
|
2462
|
-
if (mappedLocations.length > 0) {
|
|
2463
|
-
const loc = mappedLocations[0];
|
|
2464
|
-
assert(loc !== void 0, "array with length > 0 has element 0");
|
|
2465
|
-
return getMappedSnippets(loc, snippetLevel, rootDirectory);
|
|
2466
|
-
}
|
|
2467
|
-
if (location !== void 0) return getFallbackSnippet(location);
|
|
2468
|
-
return [];
|
|
2469
|
-
}
|
|
2470
|
-
function formatAgentFailure(test, filePath, options, snippetLevel) {
|
|
2471
|
-
const lines = [];
|
|
2472
|
-
for (const originalMessage of test.failureMessages) {
|
|
2473
|
-
let mappedLocations = [];
|
|
2474
|
-
let message = originalMessage;
|
|
2475
|
-
if (options.sourceMapper !== void 0) ({locations: mappedLocations, message} = options.sourceMapper.mapFailureWithLocations(originalMessage));
|
|
2476
|
-
const parsed = parseErrorMessage(originalMessage);
|
|
2477
|
-
const location = findFailureLocation(mappedLocations, message);
|
|
2478
|
-
const relativePath = makeRelative$1(location?.path ?? filePath, options.rootDir);
|
|
2479
|
-
const lineInfo = location?.line !== void 0 ? `:${location.line}` : "";
|
|
2480
|
-
const ancestors = test.ancestorTitles.length > 0 ? ` > ${test.ancestorTitles.join(" > ")}` : "";
|
|
2481
|
-
lines.push(` FAIL ${relativePath}${lineInfo}${ancestors} > ${test.title}`);
|
|
2482
|
-
if (parsed.snapshotDiff !== void 0) lines.push(parsed.snapshotDiff);
|
|
2483
|
-
else if (parsed.expected !== void 0 && parsed.received !== void 0) lines.push(`Expected: ${parsed.expected}`, `Received: ${parsed.received}`);
|
|
2484
|
-
const snippets = getFailureSnippets(mappedLocations, location, snippetLevel, options.rootDir);
|
|
2485
|
-
for (const snippet of snippets) lines.push(snippet);
|
|
2486
|
-
lines.push("");
|
|
2487
|
-
}
|
|
2488
|
-
return lines.join("\n");
|
|
2489
|
-
}
|
|
2490
|
-
function formatFailures(result, totalFailures, options) {
|
|
2491
|
-
const lines = [];
|
|
2492
|
-
const failures = collectFailedTests(result, options.sourceMapper);
|
|
2493
|
-
const snippetLevel = getSnippetLevel(totalFailures);
|
|
2494
|
-
for (const [index, { filePath, test }] of failures.entries()) {
|
|
2495
|
-
if (index >= options.maxFailures) {
|
|
2496
|
-
lines.push(`... ${result.numFailedTests - index} more failures omitted`, "");
|
|
2497
|
-
break;
|
|
2498
|
-
}
|
|
2499
|
-
lines.push(formatAgentFailure(test, filePath, options, snippetLevel));
|
|
2500
|
-
}
|
|
2501
|
-
return lines;
|
|
2502
|
-
}
|
|
2503
|
-
function formatAgentProjectHeader(displayName, result, options) {
|
|
2504
|
-
const execErrors = result.testResults.filter(hasExecError);
|
|
2505
|
-
const hasFailures = result.numFailedTests > 0 || execErrors.length > 0;
|
|
2506
|
-
const failedFiles = result.testResults.filter((file) => file.numFailingTests > 0 || hasExecError(file)).length;
|
|
2507
|
-
const skippedFiles = result.testResults.filter((file) => file.numFailingTests === 0 && file.numPassingTests === 0 && !hasExecError(file)).length;
|
|
2508
|
-
const passedFiles = result.testResults.length - failedFiles - skippedFiles;
|
|
2509
|
-
const fileParts = [];
|
|
2510
|
-
if (passedFiles > 0) fileParts.push(`${passedFiles} passed`);
|
|
2511
|
-
if (failedFiles > 0) fileParts.push(`${failedFiles} failed`);
|
|
2512
|
-
if (skippedFiles > 0) fileParts.push(`${skippedFiles} skipped`);
|
|
2513
|
-
const lines = [`▶ ${displayName} ${fileParts.join(" | ")} (${result.numTotalTests} tests)`];
|
|
2514
|
-
if (hasFailures) lines.push(...formatFileHeaders(result, options));
|
|
2515
|
-
return lines;
|
|
2516
|
-
}
|
|
2517
|
-
function collectMultiProjectStats(projects) {
|
|
2518
|
-
const stats = {
|
|
2519
|
-
allExecErrors: [],
|
|
2520
|
-
totalFailed: 0,
|
|
2521
|
-
totalFailedFiles: 0,
|
|
2522
|
-
totalPassed: 0,
|
|
2523
|
-
totalPassedFiles: 0,
|
|
2524
|
-
totalPending: 0,
|
|
2525
|
-
totalSkippedFiles: 0,
|
|
2526
|
-
totalTests: 0
|
|
2527
|
-
};
|
|
2528
|
-
for (const { result } of projects) {
|
|
2529
|
-
const failedFiles = result.testResults.filter((file) => file.numFailingTests > 0 || hasExecError(file)).length;
|
|
2530
|
-
const skippedFiles = result.testResults.filter((file) => file.numFailingTests === 0 && file.numPassingTests === 0 && !hasExecError(file)).length;
|
|
2531
|
-
stats.totalFailed += result.numFailedTests;
|
|
2532
|
-
stats.totalPassed += result.numPassedTests;
|
|
2533
|
-
stats.totalPending += result.numPendingTests;
|
|
2534
|
-
stats.totalTests += result.numTotalTests;
|
|
2535
|
-
stats.totalFailedFiles += failedFiles;
|
|
2536
|
-
stats.totalSkippedFiles += skippedFiles;
|
|
2537
|
-
stats.totalPassedFiles += result.testResults.length - failedFiles - skippedFiles;
|
|
2538
|
-
stats.allExecErrors.push(...result.testResults.filter(hasExecError));
|
|
2539
|
-
}
|
|
2540
|
-
return stats;
|
|
2541
|
-
}
|
|
2542
|
-
function formatMultiProjectFailures(projects, stats, options) {
|
|
2543
|
-
const totalFailures = stats.totalFailed + stats.allExecErrors.length;
|
|
2544
|
-
const lines = [
|
|
2545
|
-
"",
|
|
2546
|
-
`${"⎯".repeat(3)} Failed Tests ${totalFailures} ${"⎯".repeat(3)}`,
|
|
2547
|
-
""
|
|
2548
|
-
];
|
|
2549
|
-
for (const { result } of projects) if (result.numFailedTests > 0) lines.push(...formatFailures(result, totalFailures, options));
|
|
2550
|
-
for (const file of stats.allExecErrors) lines.push(...formatExecError(file, options));
|
|
2551
|
-
const hints = formatAgentLogHints(options);
|
|
2552
|
-
if (hints !== "") lines.push(hints);
|
|
2553
|
-
return lines;
|
|
2554
|
-
}
|
|
2555
|
-
function formatMultiProjectSummary(stats, options) {
|
|
2556
|
-
const lines = [];
|
|
2557
|
-
const fileParts = [];
|
|
2558
|
-
if (stats.totalFailedFiles > 0) fileParts.push(`${stats.totalFailedFiles} failed`);
|
|
2559
|
-
if (stats.totalPassedFiles > 0) fileParts.push(`${stats.totalPassedFiles} passed`);
|
|
2560
|
-
if (stats.totalSkippedFiles > 0) fileParts.push(`${stats.totalSkippedFiles} skipped`);
|
|
2561
|
-
const totalFiles = stats.totalFailedFiles + stats.totalPassedFiles + stats.totalSkippedFiles;
|
|
2562
|
-
lines.push(` Test Files ${fileParts.join(" | ")} (${totalFiles})`);
|
|
2563
|
-
const testParts = [];
|
|
2564
|
-
if (stats.totalFailed > 0) testParts.push(`${stats.totalFailed} failed`);
|
|
2565
|
-
if (stats.totalPassed > 0) testParts.push(`${stats.totalPassed} passed`);
|
|
2566
|
-
if (stats.totalPending > 0) testParts.push(`${stats.totalPending} skipped`);
|
|
2567
|
-
lines.push(` Tests ${testParts.join(" | ")} (${stats.totalTests})`);
|
|
2568
|
-
if (options.typeErrorCount !== void 0) lines.push(`Type Errors ${formatTypeErrorLabel(options.typeErrorCount)}`);
|
|
2569
|
-
return lines;
|
|
2570
|
-
}
|
|
2571
|
-
//#endregion
|
|
2572
|
-
//#region src/formatters/json.ts
|
|
2573
|
-
function formatJson(result) {
|
|
2574
|
-
return JSON.stringify(result, null, 2);
|
|
2575
|
-
}
|
|
2576
|
-
async function writeJsonFile(result, filePath) {
|
|
2577
|
-
const absolutePath = path$1.resolve(filePath);
|
|
2578
|
-
const directoryPath = path$1.dirname(absolutePath);
|
|
2579
|
-
if (!fs$1.existsSync(directoryPath)) fs$1.mkdirSync(directoryPath, { recursive: true });
|
|
2580
|
-
fs$1.writeFileSync(absolutePath, formatJson(result), "utf8");
|
|
2581
|
-
}
|
|
2582
|
-
//#endregion
|
|
2583
|
-
//#region src/formatters/utils.ts
|
|
2584
|
-
/**
|
|
2585
|
-
* Find the options object for a named formatter in a resolved formatter list.
|
|
2586
|
-
* Returns `{}` if the formatter is present without options, or `undefined` if absent.
|
|
2587
|
-
*/
|
|
2588
|
-
function findFormatterOptions(formatters, name) {
|
|
2589
|
-
for (const entry of formatters) {
|
|
2590
|
-
if (entry === name) return {};
|
|
2591
|
-
if (Array.isArray(entry) && entry[0] === name) return entry[1];
|
|
2592
|
-
}
|
|
2593
|
-
}
|
|
2594
|
-
//#endregion
|
|
2595
|
-
//#region src/snapshot/path-resolver.ts
|
|
2596
|
-
function createSnapshotPathResolver(config) {
|
|
2597
|
-
const rojoMappings = buildMappings(config.rojoProject.tree, "");
|
|
2598
|
-
const tsconfigMappings = config.mappings ?? [];
|
|
2599
|
-
return { resolve(virtualPath) {
|
|
2600
|
-
const normalized = virtualPath.replaceAll("\\", "/");
|
|
2601
|
-
for (const [prefix, basePath] of rojoMappings) {
|
|
2602
|
-
if (!normalized.startsWith(`${prefix}/`) && normalized !== prefix) continue;
|
|
2603
|
-
const result = `${basePath}/${normalized.slice(prefix.length + 1)}`;
|
|
2604
|
-
const mapping = findMapping(result, tsconfigMappings);
|
|
2605
|
-
if (mapping !== void 0) return {
|
|
2606
|
-
filePath: replacePrefix(result, mapping.outDir, mapping.rootDir).replace(/^\.\//, ""),
|
|
2607
|
-
mapping
|
|
2608
|
-
};
|
|
2609
|
-
return { filePath: result };
|
|
2610
|
-
}
|
|
2611
|
-
} };
|
|
2612
|
-
}
|
|
2613
|
-
function buildMappings(tree, prefix) {
|
|
2614
|
-
const mappings = [];
|
|
2615
|
-
for (const [key, value] of Object.entries(tree)) {
|
|
2616
|
-
if (key.startsWith("$") || typeof value !== "object") continue;
|
|
2617
|
-
const dataModelPath = prefix ? `${prefix}/${key}` : key;
|
|
2618
|
-
const node = value;
|
|
2619
|
-
if (typeof node.$path === "string") mappings.push([dataModelPath, node.$path]);
|
|
2620
|
-
mappings.push(...buildMappings(node, dataModelPath));
|
|
2621
|
-
}
|
|
2622
|
-
mappings.sort((a, b) => b[0].length - a[0].length);
|
|
2623
|
-
return mappings;
|
|
2624
|
-
}
|
|
2625
|
-
//#endregion
|
|
2626
|
-
//#region src/types/rojo.ts
|
|
2627
|
-
const rojoProjectSchema = type({
|
|
2628
|
-
"name": "string",
|
|
2629
|
-
"servePort?": "number.integer",
|
|
2630
|
-
"tree": "object"
|
|
2631
|
-
}).as();
|
|
2632
|
-
//#endregion
|
|
2633
|
-
//#region src/executor.ts
|
|
2634
|
-
function isLuauProject(testFiles, tsconfigMappings) {
|
|
2635
|
-
if (tsconfigMappings.length > 0) return false;
|
|
2636
|
-
if (testFiles.some((file) => /\.tsx?$/.test(file))) return false;
|
|
2637
|
-
return true;
|
|
2638
|
-
}
|
|
2639
|
-
function resolveAllTsconfigMappings(projectRoot) {
|
|
2640
|
-
const resolvedRoot = path$1.resolve(projectRoot);
|
|
2641
|
-
let files;
|
|
2642
|
-
try {
|
|
2643
|
-
files = fs$1.readdirSync(resolvedRoot).filter((file) => /^tsconfig.*\.json$/i.test(file));
|
|
2644
|
-
} catch {
|
|
2645
|
-
return [];
|
|
2646
|
-
}
|
|
2647
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2648
|
-
const mappings = [];
|
|
2649
|
-
for (const file of files) {
|
|
2650
|
-
const compilerOptions = getTsconfig(resolvedRoot, file)?.config.compilerOptions;
|
|
2651
|
-
if (compilerOptions?.outDir === void 0) continue;
|
|
2652
|
-
const parsed = parseTsconfigMappings(compilerOptions);
|
|
2653
|
-
for (const entry of parsed) {
|
|
2654
|
-
const key = `${entry.outDir}:${entry.rootDir}`;
|
|
2655
|
-
if (!seen.has(key)) {
|
|
2656
|
-
seen.add(key);
|
|
2657
|
-
mappings.push(entry);
|
|
2658
|
-
}
|
|
2659
|
-
}
|
|
2660
|
-
}
|
|
2661
|
-
mappings.sort((a, b) => b.outDir.length - a.outDir.length);
|
|
2662
|
-
return mappings;
|
|
2663
|
-
}
|
|
2664
|
-
function resolveTsconfigDirectories(projectRoot) {
|
|
2665
|
-
const tsconfig = getTsconfig(projectRoot, "tsconfig.lib.json") ?? getTsconfig(projectRoot);
|
|
2666
|
-
const tsconfigDirectory = tsconfig !== null ? path$1.dirname(path$1.resolve(tsconfig.path)) : void 0;
|
|
2667
|
-
const resolvedRoot = path$1.resolve(projectRoot);
|
|
2668
|
-
if (!(tsconfigDirectory?.startsWith(resolvedRoot) === true) || tsconfig?.config.compilerOptions === void 0) return {
|
|
2669
|
-
outDir: void 0,
|
|
2670
|
-
rootDir: void 0
|
|
2671
|
-
};
|
|
2672
|
-
const outDirectory = tsconfig.config.compilerOptions.outDir ?? "out";
|
|
2673
|
-
const rootDirectory = tsconfig.config.compilerOptions.rootDir ?? "src";
|
|
2674
|
-
return {
|
|
2675
|
-
outDir: normalizeDirectoryPath(outDirectory),
|
|
2676
|
-
rootDir: normalizeDirectoryPath(rootDirectory)
|
|
2677
|
-
};
|
|
2678
|
-
}
|
|
2679
|
-
function formatExecuteOutput(options) {
|
|
2680
|
-
const { config, result, sourceMapper, timing, version } = options;
|
|
2681
|
-
if (config.silent) return "";
|
|
2682
|
-
const resolvedOutputFile = config.outputFile !== void 0 ? path$1.resolve(config.outputFile) : void 0;
|
|
2683
|
-
const resolvedGameOutput = config.gameOutput !== void 0 ? path$1.resolve(config.gameOutput) : void 0;
|
|
2684
|
-
const agentOptions = findFormatterOptions(config.formatters ?? [], "agent");
|
|
2685
|
-
if (agentOptions !== void 0 && !config.verbose) return formatAgent(result, {
|
|
2686
|
-
gameOutput: resolvedGameOutput,
|
|
2687
|
-
maxFailures: agentOptions.maxFailures ?? 10,
|
|
2688
|
-
outputFile: resolvedOutputFile,
|
|
2689
|
-
rootDir: config.rootDir,
|
|
2690
|
-
sourceMapper
|
|
2691
|
-
});
|
|
2692
|
-
if (findFormatterOptions(config.formatters ?? [], "json") !== void 0) return formatJson(result);
|
|
2693
|
-
return formatResult(result, timing, {
|
|
2694
|
-
collectCoverage: config.collectCoverage,
|
|
2695
|
-
color: config.color,
|
|
2696
|
-
gameOutput: resolvedGameOutput,
|
|
2697
|
-
outputFile: resolvedOutputFile,
|
|
2698
|
-
rootDir: config.rootDir,
|
|
2699
|
-
showLuau: config.showLuau,
|
|
2700
|
-
sourceMapper,
|
|
2701
|
-
verbose: config.verbose,
|
|
2702
|
-
version
|
|
2703
|
-
});
|
|
2704
|
-
}
|
|
2705
|
-
/**
|
|
2706
|
-
* Build a `ProjectJob` with `snapshotFormat` resolved per-project. Each job
|
|
2707
|
-
* carries its own config so the Luau runner never re-resolves or shares format
|
|
2708
|
-
* state across projects (fixes the spike's snapshot-diff regression — C1).
|
|
2709
|
-
*/
|
|
2710
|
-
function buildProjectJob(parameters) {
|
|
2711
|
-
const tsconfigMappings = resolveAllTsconfigMappings(parameters.config.rootDir);
|
|
2712
|
-
const luauProject = isLuauProject(parameters.testFiles, tsconfigMappings);
|
|
2713
|
-
return {
|
|
2714
|
-
config: applySnapshotFormatDefaults(parameters.config, luauProject),
|
|
2715
|
-
displayColor: parameters.displayColor,
|
|
2716
|
-
displayName: parameters.displayName ?? "",
|
|
2717
|
-
testFiles: parameters.testFiles
|
|
2718
|
-
};
|
|
2719
|
-
}
|
|
2720
|
-
/**
|
|
2721
|
-
* Thin wrapper over `backend.runTests`. Fires exactly once per CLI invocation
|
|
2722
|
-
* with a full `ProjectJob[]` envelope. Returns the raw `BackendResult`.
|
|
2723
|
-
*/
|
|
2724
|
-
async function executeBackend(backend, jobs, parallel) {
|
|
2725
|
-
return backend.runTests({
|
|
2726
|
-
jobs,
|
|
2727
|
-
parallel
|
|
2728
|
-
});
|
|
2729
|
-
}
|
|
2730
|
-
/**
|
|
2731
|
-
* Process a single `ProjectBackendResult` into an `ExecuteResult`: writes
|
|
2732
|
-
* snapshots, builds the source mapper, resolves test-file paths, and renders
|
|
2733
|
-
* formatter output. Called once per job.
|
|
2734
|
-
*/
|
|
2735
|
-
function processProjectResult(entry, options) {
|
|
2736
|
-
const { backendTiming, config, deferFormatting, startTime, version } = options;
|
|
2737
|
-
const { coverageData, gameOutput, luauTiming, result, setupMs, snapshotWrites } = entry;
|
|
2738
|
-
const tsconfigMappings = resolveAllTsconfigMappings(config.rootDir);
|
|
2739
|
-
if (snapshotWrites !== void 0) writeSnapshots(snapshotWrites, config, tsconfigMappings);
|
|
2740
|
-
const testsMs = calculateTestsMs(result.testResults);
|
|
2741
|
-
const sourceMapper = config.sourceMap ? buildSourceMapper(config, tsconfigMappings) : void 0;
|
|
2742
|
-
resolveTestFilePaths(result, sourceMapper);
|
|
2743
|
-
const totalMs = Date.now() - startTime;
|
|
2744
|
-
const timing = {
|
|
2745
|
-
executionMs: backendTiming.executionMs,
|
|
2746
|
-
setupMs,
|
|
2747
|
-
startTime,
|
|
2748
|
-
testsMs,
|
|
2749
|
-
totalMs,
|
|
2750
|
-
uploadCached: backendTiming.uploadCached,
|
|
2751
|
-
uploadMs: backendTiming.uploadMs
|
|
2752
|
-
};
|
|
2753
|
-
const output = deferFormatting !== true ? formatExecuteOutput({
|
|
2754
|
-
config,
|
|
2755
|
-
result,
|
|
2756
|
-
sourceMapper,
|
|
2757
|
-
timing,
|
|
2758
|
-
version
|
|
2759
|
-
}) : "";
|
|
2760
|
-
if (luauTiming !== void 0) printLuauTiming(luauTiming);
|
|
2761
|
-
return {
|
|
2762
|
-
coverageData,
|
|
2763
|
-
exitCode: result.success ? 0 : 1,
|
|
2764
|
-
gameOutput,
|
|
2765
|
-
output,
|
|
2766
|
-
result,
|
|
2767
|
-
sourceMapper,
|
|
2768
|
-
timing
|
|
2769
|
-
};
|
|
2770
|
-
}
|
|
2771
|
-
/**
|
|
2772
|
-
* Single-project convenience wrapper: builds a length-1 jobs array, fires
|
|
2773
|
-
* `executeBackend` once, and maps the single entry through
|
|
2774
|
-
* `processProjectResult`. Multi-project callers drive `executeBackend` +
|
|
2775
|
-
* `processProjectResult` directly from `cli.ts`.
|
|
2776
|
-
*/
|
|
2777
|
-
async function execute(options) {
|
|
2778
|
-
const startTime = Date.now();
|
|
2779
|
-
const job = buildProjectJob({
|
|
2780
|
-
config: options.config,
|
|
2781
|
-
testFiles: options.testFiles
|
|
2782
|
-
});
|
|
2783
|
-
const { results, timing: backendTiming } = await executeBackend(options.backend, [job]);
|
|
2784
|
-
const first = results[0];
|
|
2785
|
-
return processProjectResult(first, {
|
|
2786
|
-
backendTiming,
|
|
2787
|
-
config: job.config,
|
|
2788
|
-
deferFormatting: options.deferFormatting,
|
|
2789
|
-
startTime,
|
|
2790
|
-
version: options.version
|
|
2791
|
-
});
|
|
2792
|
-
}
|
|
2793
|
-
function normalizeDirectoryPath(directory) {
|
|
2794
|
-
return path$1.normalize(directory).replaceAll("\\", "/");
|
|
2795
|
-
}
|
|
2796
|
-
function parseTsconfigMappings(options) {
|
|
2797
|
-
const outDirectory = normalizeDirectoryPath(options.outDir ?? "out");
|
|
2798
|
-
if (options.rootDirs !== void 0 && options.rootDirs.length > 0) return [{
|
|
2799
|
-
outDir: outDirectory,
|
|
2800
|
-
rootDir: options.rootDirs.map((directory) => normalizeDirectoryPath(directory)).reduce((ancestor, directory) => {
|
|
2801
|
-
const parts = ancestor.split("/");
|
|
2802
|
-
const directoryParts = directory.split("/");
|
|
2803
|
-
let common = 0;
|
|
2804
|
-
while (common < parts.length && common < directoryParts.length && parts[common] === directoryParts[common]) common++;
|
|
2805
|
-
return parts.slice(0, common).join("/");
|
|
2806
|
-
}) || "."
|
|
2807
|
-
}];
|
|
2808
|
-
if (options.rootDir === null) return [];
|
|
2809
|
-
return [{
|
|
2810
|
-
outDir: outDirectory,
|
|
2811
|
-
rootDir: normalizeDirectoryPath(options.rootDir ?? "src")
|
|
2812
|
-
}];
|
|
2813
|
-
}
|
|
2814
|
-
function findRojoProject(rootDirectory) {
|
|
2815
|
-
const defaultPath = path$1.join(rootDirectory, "default.project.json");
|
|
2816
|
-
if (fs$1.existsSync(defaultPath)) return defaultPath;
|
|
2817
|
-
const projectFile = fs$1.readdirSync(rootDirectory).find((file) => file.endsWith(".project.json"));
|
|
2818
|
-
return projectFile !== void 0 ? path$1.join(rootDirectory, projectFile) : void 0;
|
|
2819
|
-
}
|
|
2820
|
-
function buildSourceMapper(config, tsconfigMappings) {
|
|
2821
|
-
const rojoProjectPath = config.rojoProject ?? findRojoProject(config.rootDir);
|
|
2822
|
-
if (rojoProjectPath === void 0 || !fs$1.existsSync(rojoProjectPath)) return;
|
|
2823
|
-
try {
|
|
2824
|
-
const rojoResult = rojoProjectSchema(JSON.parse(fs$1.readFileSync(rojoProjectPath, "utf-8")));
|
|
2825
|
-
if (rojoResult instanceof type.errors) return;
|
|
2826
|
-
const resolvedTree = resolveNestedProjects(rojoResult.tree, path$1.dirname(rojoProjectPath));
|
|
2827
|
-
return createSourceMapper({
|
|
2828
|
-
mappings: tsconfigMappings,
|
|
2829
|
-
rojoProject: {
|
|
2830
|
-
...rojoResult,
|
|
2831
|
-
tree: resolvedTree
|
|
2832
|
-
}
|
|
2833
|
-
});
|
|
2834
|
-
} catch {
|
|
2835
|
-
return;
|
|
2836
|
-
}
|
|
2837
|
-
}
|
|
2838
|
-
function resolveTestFilePaths(result, sourceMapper) {
|
|
2839
|
-
if (sourceMapper === void 0) return;
|
|
2840
|
-
for (const file of result.testResults) file.testFilePath = sourceMapper.resolveTestFilePath(file.testFilePath) ?? file.testFilePath;
|
|
2841
|
-
}
|
|
2842
|
-
function calculateTestsMs(testResults) {
|
|
2843
|
-
let total = 0;
|
|
2844
|
-
for (const file of testResults) for (const test of file.testResults) if (test.duration !== void 0) total += test.duration;
|
|
2845
|
-
return total;
|
|
2846
|
-
}
|
|
2847
|
-
function printLuauTiming(timing) {
|
|
2848
|
-
let total = 0;
|
|
2849
|
-
for (const [phase, seconds] of Object.entries(timing)) {
|
|
2850
|
-
const ms = Math.round(seconds * 1e3);
|
|
2851
|
-
total += ms;
|
|
2852
|
-
process.stderr.write(`[TIMING] ${phase}: ${String(ms)}ms\n`);
|
|
2853
|
-
}
|
|
2854
|
-
process.stderr.write(`[TIMING] total: ${String(total)}ms\n`);
|
|
2855
|
-
}
|
|
2856
|
-
const instrumentedFileRecordSchema = type({
|
|
2857
|
-
"key": "string",
|
|
2858
|
-
"branchCount?": "number",
|
|
2859
|
-
"coverageMapPath": "string",
|
|
2860
|
-
"functionCount?": "number",
|
|
2861
|
-
"instrumentedLuauPath": "string",
|
|
2862
|
-
"originalLuauPath": "string",
|
|
2863
|
-
"sourceMapPath": "string",
|
|
2864
|
-
"statementCount": "number"
|
|
2865
|
-
});
|
|
2866
|
-
const coverageManifestSchema = type({
|
|
2867
|
-
files: type("Record<string, unknown>").pipe((files) => {
|
|
2868
|
-
const validated = {};
|
|
2869
|
-
const skipped = [];
|
|
2870
|
-
for (const [key, value] of Object.entries(files)) {
|
|
2871
|
-
const parsed = instrumentedFileRecordSchema(value);
|
|
2872
|
-
if (parsed instanceof type.errors) skipped.push(key);
|
|
2873
|
-
else validated[key] = parsed;
|
|
2874
|
-
}
|
|
2875
|
-
if (skipped.length > 0) process.stderr.write(`Warning: ${skipped.length} file record(s) in coverage manifest failed validation and were skipped: ${skipped.join(", ")}\n`);
|
|
2876
|
-
return validated;
|
|
2877
|
-
}),
|
|
2878
|
-
generatedAt: "string",
|
|
2879
|
-
luauRoots: "string[]",
|
|
2880
|
-
shadowDir: "string",
|
|
2881
|
-
version: type.unit(1)
|
|
2882
|
-
});
|
|
2883
|
-
function loadCoverageManifest(rootDirectory) {
|
|
2884
|
-
const manifestPath = path$1.join(rootDirectory, ".jest-roblox-coverage", "manifest.json");
|
|
2885
|
-
try {
|
|
2886
|
-
const raw = fs$1.readFileSync(manifestPath, "utf-8");
|
|
2887
|
-
const parsed = coverageManifestSchema(JSON.parse(raw));
|
|
2888
|
-
if (parsed instanceof type.errors) {
|
|
2889
|
-
process.stderr.write(`Warning: Coverage manifest is invalid (re-run \`jest-roblox instrument\`): ${parsed.summary}\n`);
|
|
2890
|
-
return;
|
|
2891
|
-
}
|
|
2892
|
-
return parsed;
|
|
2893
|
-
} catch (err) {
|
|
2894
|
-
if (err instanceof SyntaxError) process.stderr.write("Warning: Coverage manifest is malformed JSON (re-run `jest-roblox instrument`)\n");
|
|
2895
|
-
return;
|
|
2896
|
-
}
|
|
2897
|
-
}
|
|
2898
|
-
function writeSnapshots(snapshotWrites, config, tsconfigMappings) {
|
|
2899
|
-
const rojoProjectPath = config.rojoProject ?? findRojoProject(config.rootDir);
|
|
2900
|
-
if (rojoProjectPath === void 0 || !fs$1.existsSync(rojoProjectPath)) {
|
|
2901
|
-
process.stderr.write("Warning: Cannot write snapshots - no rojo project found\n");
|
|
2902
|
-
return;
|
|
2903
|
-
}
|
|
2904
|
-
try {
|
|
2905
|
-
const rojoResult = rojoProjectSchema(JSON.parse(fs$1.readFileSync(rojoProjectPath, "utf-8")));
|
|
2906
|
-
if (rojoResult instanceof type.errors) {
|
|
2907
|
-
process.stderr.write("Warning: Cannot write snapshots - invalid rojo project\n");
|
|
2908
|
-
return;
|
|
2909
|
-
}
|
|
2910
|
-
const resolvedTree = resolveNestedProjects(rojoResult.tree, path$1.dirname(rojoProjectPath));
|
|
2911
|
-
const resolver = createSnapshotPathResolver({
|
|
2912
|
-
mappings: tsconfigMappings,
|
|
2913
|
-
rojoProject: {
|
|
2914
|
-
...rojoResult,
|
|
2915
|
-
tree: resolvedTree
|
|
2916
|
-
}
|
|
2917
|
-
});
|
|
2918
|
-
let written = 0;
|
|
2919
|
-
for (const [virtualPath, content] of Object.entries(snapshotWrites)) {
|
|
2920
|
-
const resolved = resolver.resolve(virtualPath);
|
|
2921
|
-
if (resolved === void 0) {
|
|
2922
|
-
process.stderr.write(`Warning: Cannot resolve snapshot path: ${virtualPath}\n`);
|
|
2923
|
-
continue;
|
|
2924
|
-
}
|
|
2925
|
-
const absolutePath = path$1.resolve(config.rootDir, resolved.filePath);
|
|
2926
|
-
fs$1.mkdirSync(path$1.dirname(absolutePath), { recursive: true });
|
|
2927
|
-
fs$1.writeFileSync(absolutePath, content);
|
|
2928
|
-
const { filePath, mapping } = resolved;
|
|
2929
|
-
if (mapping !== void 0) {
|
|
2930
|
-
const outPath = mapping.outDir + filePath.slice(mapping.rootDir.length);
|
|
2931
|
-
const absoluteOutPath = path$1.resolve(config.rootDir, outPath);
|
|
2932
|
-
fs$1.mkdirSync(path$1.dirname(absoluteOutPath), { recursive: true });
|
|
2933
|
-
fs$1.writeFileSync(absoluteOutPath, content);
|
|
2934
|
-
}
|
|
2935
|
-
written++;
|
|
2936
|
-
}
|
|
2937
|
-
if (written > 0) process.stderr.write(`Wrote ${String(written)} snapshot file${written === 1 ? "" : "s"}\n`);
|
|
2938
|
-
} catch (err) {
|
|
2939
|
-
if (err instanceof SyntaxError) process.stderr.write(formatBanner({
|
|
2940
|
-
body: [color.red(`Failed to parse rojo project: ${err.message}`), ` ${color.dim("File:")} ${rojoProjectPath}`],
|
|
2941
|
-
level: "warn",
|
|
2942
|
-
title: "Snapshot Warning"
|
|
2943
|
-
}));
|
|
2944
|
-
else process.stderr.write(`Warning: Failed to write snapshot files: ${String(err)}\n`);
|
|
2945
|
-
}
|
|
2946
|
-
}
|
|
2947
|
-
//#endregion
|
|
2948
|
-
//#region packages/luau-ast/dist/index.mjs
|
|
2949
|
-
function visitExpression(expression, visitor) {
|
|
2950
|
-
if (visitor.visitExpr?.(expression) === false) return;
|
|
2951
|
-
const { tag } = expression;
|
|
2952
|
-
switch (tag) {
|
|
2953
|
-
case "binary":
|
|
2954
|
-
visitExprBinary(expression, visitor);
|
|
2955
|
-
break;
|
|
2956
|
-
case "boolean":
|
|
2957
|
-
visitor.visitExprConstantBool?.(expression);
|
|
2958
|
-
break;
|
|
2959
|
-
case "call":
|
|
2960
|
-
visitExprCall(expression, visitor);
|
|
2961
|
-
break;
|
|
2962
|
-
case "cast":
|
|
2963
|
-
visitExprTypeAssertion(expression, visitor);
|
|
2964
|
-
break;
|
|
2965
|
-
case "conditional":
|
|
2966
|
-
visitExprIfElse(expression, visitor);
|
|
2967
|
-
break;
|
|
2968
|
-
case "function":
|
|
2969
|
-
visitExprFunction(expression, visitor);
|
|
2970
|
-
break;
|
|
2971
|
-
case "global":
|
|
2972
|
-
visitor.visitExprGlobal?.(expression);
|
|
2973
|
-
break;
|
|
2974
|
-
case "group":
|
|
2975
|
-
visitExprGroup(expression, visitor);
|
|
2976
|
-
break;
|
|
2977
|
-
case "index":
|
|
2978
|
-
visitExprIndexExpr(expression, visitor);
|
|
2979
|
-
break;
|
|
2980
|
-
case "indexname":
|
|
2981
|
-
visitExprIndexName(expression, visitor);
|
|
2982
|
-
break;
|
|
2983
|
-
case "instantiate":
|
|
2984
|
-
visitExprInstantiate(expression, visitor);
|
|
2985
|
-
break;
|
|
2986
|
-
case "interpolatedstring":
|
|
2987
|
-
visitExprInterpString(expression, visitor);
|
|
2988
|
-
break;
|
|
2989
|
-
case "local":
|
|
2990
|
-
visitor.visitExprLocal?.(expression);
|
|
2991
|
-
break;
|
|
2992
|
-
case "nil":
|
|
2993
|
-
visitor.visitExprConstantNil?.(expression);
|
|
2994
|
-
break;
|
|
2995
|
-
case "number":
|
|
2996
|
-
visitor.visitExprConstantNumber?.(expression);
|
|
2997
|
-
break;
|
|
2998
|
-
case "string":
|
|
2999
|
-
visitor.visitExprConstantString?.(expression);
|
|
3000
|
-
break;
|
|
3001
|
-
case "table":
|
|
3002
|
-
visitExprTable(expression, visitor);
|
|
3003
|
-
break;
|
|
3004
|
-
case "unary":
|
|
3005
|
-
visitExprUnary(expression, visitor);
|
|
3006
|
-
break;
|
|
3007
|
-
case "vararg":
|
|
3008
|
-
visitor.visitExprVarargs?.(expression);
|
|
3009
|
-
break;
|
|
3010
|
-
default: break;
|
|
3011
|
-
}
|
|
3012
|
-
visitor.visitExprEnd?.(expression);
|
|
3013
|
-
}
|
|
3014
|
-
function visitStatement(statement, visitor) {
|
|
3015
|
-
const { tag } = statement;
|
|
3016
|
-
switch (tag) {
|
|
3017
|
-
case "assign":
|
|
3018
|
-
visitStatAssign(statement, visitor);
|
|
3019
|
-
break;
|
|
3020
|
-
case "block":
|
|
3021
|
-
visitStatBlock(statement, visitor);
|
|
3022
|
-
break;
|
|
3023
|
-
case "break":
|
|
3024
|
-
visitor.visitStatBreak?.(statement);
|
|
3025
|
-
break;
|
|
3026
|
-
case "compoundassign":
|
|
3027
|
-
visitStatCompoundAssign(statement, visitor);
|
|
3028
|
-
break;
|
|
3029
|
-
case "conditional":
|
|
3030
|
-
visitStatIf(statement, visitor);
|
|
3031
|
-
break;
|
|
3032
|
-
case "continue":
|
|
3033
|
-
visitor.visitStatContinue?.(statement);
|
|
3034
|
-
break;
|
|
3035
|
-
case "do":
|
|
3036
|
-
visitStatDo(statement, visitor);
|
|
3037
|
-
break;
|
|
3038
|
-
case "expression":
|
|
3039
|
-
visitStatExpr(statement, visitor);
|
|
3040
|
-
break;
|
|
3041
|
-
case "for":
|
|
3042
|
-
visitStatFor(statement, visitor);
|
|
3043
|
-
break;
|
|
3044
|
-
case "forin":
|
|
3045
|
-
visitStatForIn(statement, visitor);
|
|
3046
|
-
break;
|
|
3047
|
-
case "function":
|
|
3048
|
-
visitStatFunction(statement, visitor);
|
|
3049
|
-
break;
|
|
3050
|
-
case "local":
|
|
3051
|
-
visitStatLocal(statement, visitor);
|
|
3052
|
-
break;
|
|
3053
|
-
case "localfunction":
|
|
3054
|
-
visitStatLocalFunction(statement, visitor);
|
|
3055
|
-
break;
|
|
3056
|
-
case "repeat":
|
|
3057
|
-
visitStatRepeat(statement, visitor);
|
|
3058
|
-
break;
|
|
3059
|
-
case "return":
|
|
3060
|
-
visitStatReturn(statement, visitor);
|
|
3061
|
-
break;
|
|
3062
|
-
case "typealias":
|
|
3063
|
-
visitor.visitStatTypeAlias?.(statement);
|
|
3064
|
-
break;
|
|
3065
|
-
case "typefunction":
|
|
3066
|
-
visitor.visitStatTypeFunction?.(statement);
|
|
3067
|
-
break;
|
|
3068
|
-
case "while":
|
|
3069
|
-
visitStatWhile(statement, visitor);
|
|
3070
|
-
break;
|
|
3071
|
-
default: break;
|
|
3072
|
-
}
|
|
3073
|
-
}
|
|
3074
|
-
function visitBlock(block, visitor) {
|
|
3075
|
-
visitStatBlock(block, visitor);
|
|
3076
|
-
}
|
|
3077
|
-
function visitPunctuated(list, visitor, apply) {
|
|
3078
|
-
for (const item of list) apply(item.node, visitor);
|
|
3079
|
-
}
|
|
3080
|
-
function visitStatBlock(block, visitor) {
|
|
3081
|
-
if (visitor.visitStatBlock?.(block) === false) return;
|
|
3082
|
-
for (const statement of block.statements) visitStatement(statement, visitor);
|
|
3083
|
-
visitor.visitStatBlockEnd?.(block);
|
|
3084
|
-
}
|
|
3085
|
-
function visitStatDo(node, visitor) {
|
|
3086
|
-
if (visitor.visitStatDo?.(node) === false) return;
|
|
3087
|
-
visitStatBlock(node.body, visitor);
|
|
3088
|
-
}
|
|
3089
|
-
function visitStatIf(node, visitor) {
|
|
3090
|
-
if (visitor.visitStatIf?.(node) === false) return;
|
|
3091
|
-
visitExpression(node.condition, visitor);
|
|
3092
|
-
visitStatBlock(node.thenBlock, visitor);
|
|
3093
|
-
for (const elseif of node.elseifs) visitElseIfStat(elseif, visitor);
|
|
3094
|
-
if (node.elseBlock) visitStatBlock(node.elseBlock, visitor);
|
|
3095
|
-
}
|
|
3096
|
-
function visitElseIfStat(node, visitor) {
|
|
3097
|
-
visitExpression(node.condition, visitor);
|
|
3098
|
-
visitStatBlock(node.thenBlock, visitor);
|
|
3099
|
-
}
|
|
3100
|
-
function visitStatWhile(node, visitor) {
|
|
3101
|
-
if (visitor.visitStatWhile?.(node) === false) return;
|
|
3102
|
-
visitExpression(node.condition, visitor);
|
|
3103
|
-
visitStatBlock(node.body, visitor);
|
|
3104
|
-
}
|
|
3105
|
-
function visitStatRepeat(node, visitor) {
|
|
3106
|
-
if (visitor.visitStatRepeat?.(node) === false) return;
|
|
3107
|
-
visitStatBlock(node.body, visitor);
|
|
3108
|
-
visitExpression(node.condition, visitor);
|
|
3109
|
-
}
|
|
3110
|
-
function visitStatReturn(node, visitor) {
|
|
3111
|
-
if (visitor.visitStatReturn?.(node) === false) return;
|
|
3112
|
-
visitPunctuated(node.expressions, visitor, visitExpression);
|
|
3113
|
-
}
|
|
3114
|
-
function visitStatLocal(node, visitor) {
|
|
3115
|
-
if (visitor.visitStatLocal?.(node) === false) return;
|
|
3116
|
-
visitPunctuated(node.values, visitor, visitExpression);
|
|
3117
|
-
}
|
|
3118
|
-
function visitStatFor(node, visitor) {
|
|
3119
|
-
if (visitor.visitStatFor?.(node) === false) return;
|
|
3120
|
-
visitExpression(node.from, visitor);
|
|
3121
|
-
visitExpression(node.to, visitor);
|
|
3122
|
-
if (node.step) visitExpression(node.step, visitor);
|
|
3123
|
-
visitStatBlock(node.body, visitor);
|
|
3124
|
-
}
|
|
3125
|
-
function visitStatForIn(node, visitor) {
|
|
3126
|
-
if (visitor.visitStatForIn?.(node) === false) return;
|
|
3127
|
-
visitPunctuated(node.values, visitor, visitExpression);
|
|
3128
|
-
visitStatBlock(node.body, visitor);
|
|
3129
|
-
}
|
|
3130
|
-
function visitStatAssign(node, visitor) {
|
|
3131
|
-
if (visitor.visitStatAssign?.(node) === false) return;
|
|
3132
|
-
visitPunctuated(node.variables, visitor, visitExpression);
|
|
3133
|
-
visitPunctuated(node.values, visitor, visitExpression);
|
|
3134
|
-
}
|
|
3135
|
-
function visitStatCompoundAssign(node, visitor) {
|
|
3136
|
-
if (visitor.visitStatCompoundAssign?.(node) === false) return;
|
|
3137
|
-
visitExpression(node.variable, visitor);
|
|
3138
|
-
visitExpression(node.value, visitor);
|
|
3139
|
-
}
|
|
3140
|
-
function visitStatExpr(node, visitor) {
|
|
3141
|
-
if (visitor.visitStatExpr?.(node) === false) return;
|
|
3142
|
-
visitExpression(node.expression, visitor);
|
|
3143
|
-
}
|
|
3144
|
-
function visitStatFunction(node, visitor) {
|
|
3145
|
-
if (visitor.visitStatFunction?.(node) === false) return;
|
|
3146
|
-
visitExpression(node.name, visitor);
|
|
3147
|
-
visitExprFunction(node.func, visitor);
|
|
3148
|
-
}
|
|
3149
|
-
function visitStatLocalFunction(node, visitor) {
|
|
3150
|
-
if (visitor.visitStatLocalFunction?.(node) === false) return;
|
|
3151
|
-
visitExprFunction(node.func, visitor);
|
|
3152
|
-
}
|
|
3153
|
-
function visitExprFunction(node, visitor) {
|
|
3154
|
-
if (visitor.visitExprFunction?.(node) === false) return;
|
|
3155
|
-
visitStatBlock(node.body, visitor);
|
|
3156
|
-
visitor.visitExprFunctionEnd?.(node);
|
|
3157
|
-
}
|
|
3158
|
-
function visitExprCall(node, visitor) {
|
|
3159
|
-
if (visitor.visitExprCall?.(node) === false) return;
|
|
3160
|
-
visitExpression(node.func, visitor);
|
|
3161
|
-
visitPunctuated(node.arguments, visitor, visitExpression);
|
|
3162
|
-
}
|
|
3163
|
-
function visitExprUnary(node, visitor) {
|
|
3164
|
-
if (visitor.visitExprUnary?.(node) === false) return;
|
|
3165
|
-
visitExpression(node.operand, visitor);
|
|
3166
|
-
}
|
|
3167
|
-
function visitExprBinary(node, visitor) {
|
|
3168
|
-
if (visitor.visitExprBinary?.(node) === false) return;
|
|
3169
|
-
visitExpression(node.lhsOperand, visitor);
|
|
3170
|
-
visitExpression(node.rhsOperand, visitor);
|
|
3171
|
-
}
|
|
3172
|
-
function visitExprTable(node, visitor) {
|
|
3173
|
-
if (visitor.visitExprTable?.(node) === false) return;
|
|
3174
|
-
for (const item of node.entries) visitTableExprItem(item, visitor);
|
|
3175
|
-
}
|
|
3176
|
-
function visitTableExprItem(node, visitor) {
|
|
3177
|
-
if (visitor.visitTableExprItem?.(node) === false) return;
|
|
3178
|
-
visitExpression(node.value, visitor);
|
|
3179
|
-
if (node.kind === "general") visitExpression(node.key, visitor);
|
|
3180
|
-
}
|
|
3181
|
-
function visitExprIndexName(node, visitor) {
|
|
3182
|
-
if (visitor.visitExprIndexName?.(node) === false) return;
|
|
3183
|
-
visitExpression(node.expression, visitor);
|
|
3184
|
-
}
|
|
3185
|
-
function visitExprIndexExpr(node, visitor) {
|
|
3186
|
-
if (visitor.visitExprIndexExpr?.(node) === false) return;
|
|
3187
|
-
visitExpression(node.expression, visitor);
|
|
3188
|
-
visitExpression(node.index, visitor);
|
|
3189
|
-
}
|
|
3190
|
-
function visitExprGroup(node, visitor) {
|
|
3191
|
-
if (visitor.visitExprGroup?.(node) === false) return;
|
|
3192
|
-
visitExpression(node.expression, visitor);
|
|
3193
|
-
}
|
|
3194
|
-
function visitExprInterpString(node, visitor) {
|
|
3195
|
-
if (visitor.visitExprInterpString?.(node) === false) return;
|
|
3196
|
-
for (const expr of node.expressions) visitExpression(expr, visitor);
|
|
3197
|
-
}
|
|
3198
|
-
function visitExprTypeAssertion(node, visitor) {
|
|
3199
|
-
if (visitor.visitExprTypeAssertion?.(node) === false) return;
|
|
3200
|
-
visitExpression(node.operand, visitor);
|
|
3201
|
-
}
|
|
3202
|
-
function visitExprIfElse(node, visitor) {
|
|
3203
|
-
if (visitor.visitExprIfElse?.(node) === false) return;
|
|
3204
|
-
visitExpression(node.condition, visitor);
|
|
3205
|
-
visitExpression(node.thenExpr, visitor);
|
|
3206
|
-
for (const elseif of node.elseifs) visitElseIfExpr(elseif, visitor);
|
|
3207
|
-
visitExpression(node.elseExpr, visitor);
|
|
3208
|
-
}
|
|
3209
|
-
function visitElseIfExpr(node, visitor) {
|
|
3210
|
-
visitExpression(node.condition, visitor);
|
|
3211
|
-
visitExpression(node.thenExpr, visitor);
|
|
3212
|
-
}
|
|
3213
|
-
function visitExprInstantiate(node, visitor) {
|
|
3214
|
-
if (visitor.visitExprInstantiate?.(node) === false) return;
|
|
3215
|
-
visitExpression(node.expr, visitor);
|
|
3216
|
-
}
|
|
3217
|
-
//#endregion
|
|
3218
|
-
//#region src/formatters/github-actions.ts
|
|
3219
|
-
const SEPARATOR = " · ";
|
|
3220
|
-
function escapeData(value) {
|
|
3221
|
-
return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
3222
|
-
}
|
|
3223
|
-
function escapeProperty(value) {
|
|
3224
|
-
return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A").replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
3225
|
-
}
|
|
3226
|
-
function formatAnnotation(annotation) {
|
|
3227
|
-
const properties = [`file=${escapeProperty(annotation.file)}`];
|
|
3228
|
-
if (annotation.line !== void 0) properties.push(`line=${String(annotation.line)}`);
|
|
3229
|
-
if (annotation.col !== void 0) properties.push(`col=${String(annotation.col)}`);
|
|
3230
|
-
if (annotation.title !== void 0) properties.push(`title=${escapeProperty(annotation.title)}`);
|
|
3231
|
-
return `::error ${properties.join(",")}::${escapeData(annotation.message)}`;
|
|
3232
|
-
}
|
|
3233
|
-
function collectAnnotations(result, options) {
|
|
3234
|
-
const annotations = [];
|
|
3235
|
-
for (const file of result.testResults) {
|
|
3236
|
-
if (hasExecError(file)) {
|
|
3237
|
-
collectExecErrorAnnotation(annotations, file, options);
|
|
3238
|
-
continue;
|
|
3239
|
-
}
|
|
3240
|
-
collectTestFailureAnnotations(annotations, file, options);
|
|
3241
|
-
}
|
|
3242
|
-
return annotations;
|
|
3243
|
-
}
|
|
3244
|
-
function formatAnnotations(result, options) {
|
|
3245
|
-
const annotations = collectAnnotations(result, options);
|
|
3246
|
-
if (annotations.length === 0) return "";
|
|
3247
|
-
return annotations.map(formatAnnotation).join("\n");
|
|
3248
|
-
}
|
|
3249
|
-
function formatJobSummary(result, options) {
|
|
3250
|
-
const fileLink = createFileLink(options);
|
|
3251
|
-
const lines = ["## Test Results\n", renderStats(result)];
|
|
3252
|
-
const failures = [];
|
|
3253
|
-
for (const file of result.testResults) {
|
|
3254
|
-
if (hasExecError(file)) {
|
|
3255
|
-
failures.push({
|
|
3256
|
-
file: makeRelative(file.testFilePath, options.workspace),
|
|
3257
|
-
title: "Test suite failed to run"
|
|
3258
|
-
});
|
|
3259
|
-
continue;
|
|
3260
|
-
}
|
|
3261
|
-
for (const test of file.testResults) {
|
|
3262
|
-
if (test.status !== "failed") continue;
|
|
3263
|
-
failures.push({
|
|
3264
|
-
file: makeRelative(file.testFilePath, options.workspace),
|
|
3265
|
-
title: test.fullName
|
|
3266
|
-
});
|
|
3267
|
-
}
|
|
3268
|
-
}
|
|
3269
|
-
if (failures.length > 0) {
|
|
3270
|
-
lines.push("### Failures\n");
|
|
3271
|
-
for (const failure of failures) {
|
|
3272
|
-
const link = fileLink(failure.file);
|
|
3273
|
-
const fileRef = link !== void 0 ? `[${failure.file}](${link})` : failure.file;
|
|
3274
|
-
lines.push(`- **${failure.title}** in ${fileRef}`);
|
|
3275
|
-
}
|
|
3276
|
-
lines.push("");
|
|
3277
|
-
}
|
|
3278
|
-
return lines.join("\n");
|
|
3279
|
-
}
|
|
3280
|
-
function resolveGitHubActionsOptions(userOptions, sourceMapper, environment = process.env) {
|
|
3281
|
-
return {
|
|
3282
|
-
repository: userOptions.jobSummary?.fileLinks?.repository ?? environment["GITHUB_REPOSITORY"],
|
|
3283
|
-
serverUrl: environment["GITHUB_SERVER_URL"],
|
|
3284
|
-
sha: userOptions.jobSummary?.fileLinks?.commitHash ?? environment["GITHUB_SHA"],
|
|
3285
|
-
sourceMapper,
|
|
3286
|
-
workspace: userOptions.jobSummary?.fileLinks?.workspacePath ?? environment["GITHUB_WORKSPACE"]
|
|
3287
|
-
};
|
|
3288
|
-
}
|
|
3289
|
-
function makeRelative(filePath, workspace) {
|
|
3290
|
-
if (workspace === void 0) return filePath;
|
|
3291
|
-
const normalized = filePath.replace(/\\/g, "/");
|
|
3292
|
-
const normalizedWorkspace = workspace.replace(/\\/g, "/").replace(/\/$/, "");
|
|
3293
|
-
if (normalized.startsWith(`${normalizedWorkspace}/`)) return normalized.slice(normalizedWorkspace.length + 1);
|
|
3294
|
-
return filePath;
|
|
3295
|
-
}
|
|
3296
|
-
function collectExecErrorAnnotation(annotations, file, options) {
|
|
3297
|
-
annotations.push({
|
|
3298
|
-
file: makeRelative(file.testFilePath, options.workspace),
|
|
3299
|
-
message: file.failureMessage,
|
|
3300
|
-
title: "Test suite failed to run"
|
|
3301
|
-
});
|
|
3302
|
-
}
|
|
3303
|
-
function collectTestFailureAnnotations(annotations, file, options) {
|
|
3304
|
-
for (const test of file.testResults) {
|
|
3305
|
-
if (test.status !== "failed") continue;
|
|
3306
|
-
const firstFailure = test.failureMessages[0] ?? "";
|
|
3307
|
-
let annotationFile = file.testFilePath;
|
|
3308
|
-
let line;
|
|
3309
|
-
let column;
|
|
3310
|
-
if (options.sourceMapper !== void 0 && firstFailure !== "") {
|
|
3311
|
-
const location = options.sourceMapper.mapFailureWithLocations(firstFailure).locations[0];
|
|
3312
|
-
if (location?.tsPath !== void 0) {
|
|
3313
|
-
annotationFile = location.tsPath;
|
|
3314
|
-
line = location.tsLine;
|
|
3315
|
-
column = location.tsColumn;
|
|
3316
|
-
} else if (location !== void 0) {
|
|
3317
|
-
annotationFile = location.luauPath;
|
|
3318
|
-
line = location.luauLine;
|
|
3319
|
-
}
|
|
3320
|
-
}
|
|
3321
|
-
annotations.push({
|
|
3322
|
-
col: column,
|
|
3323
|
-
file: makeRelative(annotationFile, options.workspace),
|
|
3324
|
-
line,
|
|
3325
|
-
message: firstFailure,
|
|
3326
|
-
title: test.fullName
|
|
3327
|
-
});
|
|
3328
|
-
}
|
|
3329
|
-
}
|
|
3330
|
-
function noun(count, singular, plural) {
|
|
3331
|
-
return count === 1 ? singular : plural;
|
|
3332
|
-
}
|
|
3333
|
-
function renderStats(result) {
|
|
3334
|
-
const failedFiles = result.testResults.filter((file) => file.numFailingTests > 0 || hasExecError(file)).length;
|
|
3335
|
-
const passedFiles = result.testResults.filter((file) => file.numFailingTests === 0 && !hasExecError(file)).length;
|
|
3336
|
-
const totalFiles = failedFiles + passedFiles;
|
|
3337
|
-
const fileInfo = [];
|
|
3338
|
-
if (failedFiles > 0) fileInfo.push(`❌ **${String(failedFiles)} ${noun(failedFiles, "failure", "failures")}**`);
|
|
3339
|
-
if (passedFiles > 0) fileInfo.push(`✅ **${String(passedFiles)} ${noun(passedFiles, "pass", "passes")}**`);
|
|
3340
|
-
fileInfo.push(`${String(totalFiles)} total`);
|
|
3341
|
-
const testInfo = [];
|
|
3342
|
-
if (result.numFailedTests > 0) testInfo.push(`❌ **${String(result.numFailedTests)} ${noun(result.numFailedTests, "failure", "failures")}**`);
|
|
3343
|
-
if (result.numPassedTests > 0) testInfo.push(`✅ **${String(result.numPassedTests)} ${noun(result.numPassedTests, "pass", "passes")}**`);
|
|
3344
|
-
const primaryTotal = result.numFailedTests + result.numPassedTests;
|
|
3345
|
-
testInfo.push(`${String(primaryTotal)} total`);
|
|
3346
|
-
let output = "### Summary\n\n";
|
|
3347
|
-
output += `- **Test Files**: ${fileInfo.join(SEPARATOR)}\n`;
|
|
3348
|
-
output += `- **Test Results**: ${testInfo.join(SEPARATOR)}\n`;
|
|
3349
|
-
const otherInfo = [];
|
|
3350
|
-
if (result.numPendingTests > 0) otherInfo.push(`${String(result.numPendingTests)} ${noun(result.numPendingTests, "skip", "skips")}`);
|
|
3351
|
-
if (result.numTodoTests !== void 0 && result.numTodoTests > 0) otherInfo.push(`${String(result.numTodoTests)} ${noun(result.numTodoTests, "todo", "todos")}`);
|
|
3352
|
-
if (otherInfo.length > 0) {
|
|
3353
|
-
const otherTotal = result.numPendingTests + (result.numTodoTests ?? 0);
|
|
3354
|
-
otherInfo.push(`${String(otherTotal)} total`);
|
|
3355
|
-
output += `- **Other**: ${otherInfo.join(SEPARATOR)}\n`;
|
|
3356
|
-
}
|
|
3357
|
-
return output;
|
|
3358
|
-
}
|
|
3359
|
-
function createFileLink(options) {
|
|
3360
|
-
const { repository, serverUrl, sha } = options;
|
|
3361
|
-
if (serverUrl === void 0 || repository === void 0 || sha === void 0) return (_filePath) => {};
|
|
3362
|
-
return (filePath) => `${serverUrl}/${repository}/blob/${sha}/${filePath}`;
|
|
3363
|
-
}
|
|
3364
|
-
//#endregion
|
|
3365
|
-
//#region src/typecheck/collect.ts
|
|
3366
|
-
const TEST_FUNCTIONS = new Set(["it", "test"]);
|
|
3367
|
-
const ALL_FUNCTIONS = new Set([...new Set(["describe", "suite"]), ...TEST_FUNCTIONS]);
|
|
3368
|
-
function collectTestDefinitions(source) {
|
|
3369
|
-
const result = parseSync("test.ts", source);
|
|
3370
|
-
const raw = [];
|
|
3371
|
-
new Visitor({ CallExpression(node) {
|
|
3372
|
-
const definition = extractDefinition(node, source);
|
|
3373
|
-
if (definition) raw.push(definition);
|
|
3374
|
-
} }).visit(result.program);
|
|
3375
|
-
raw.sort((a, b) => a.start - b.start);
|
|
3376
|
-
return buildAncestorChain(raw);
|
|
3377
|
-
}
|
|
3378
|
-
function buildAncestorChain(sorted) {
|
|
3379
|
-
const result = [];
|
|
3380
|
-
const suiteStack = [];
|
|
3381
|
-
for (const definition of sorted) {
|
|
3382
|
-
while (suiteStack.length > 0 && suiteStack.at(-1).end <= definition.start) suiteStack.pop();
|
|
3383
|
-
result.push({
|
|
3384
|
-
...definition,
|
|
3385
|
-
ancestorNames: suiteStack.map((suite) => suite.name)
|
|
3386
|
-
});
|
|
3387
|
-
if (definition.type === "suite") suiteStack.push({
|
|
3388
|
-
name: definition.name,
|
|
3389
|
-
end: definition.end
|
|
3390
|
-
});
|
|
3391
|
-
}
|
|
3392
|
-
return result;
|
|
3393
|
-
}
|
|
3394
|
-
function isStringLiteral(node) {
|
|
3395
|
-
return node.type === "Literal" && typeof node.value === "string";
|
|
3396
|
-
}
|
|
3397
|
-
function isTemplateLiteral(node) {
|
|
3398
|
-
return node.type === "TemplateLiteral";
|
|
3399
|
-
}
|
|
3400
|
-
function extractStringArgument(node, source) {
|
|
3401
|
-
if (isStringLiteral(node)) return node.value;
|
|
3402
|
-
if (isTemplateLiteral(node)) {
|
|
3403
|
-
if (node.quasis.length === 1 && node.quasis[0] !== void 0) return node.quasis[0].value.raw;
|
|
3404
|
-
return source.slice(node.start + 1, node.end - 1);
|
|
3405
|
-
}
|
|
3406
|
-
return source.slice(node.start, node.end);
|
|
3407
|
-
}
|
|
3408
|
-
function isIdentifier(node) {
|
|
3409
|
-
return node.type === "Identifier";
|
|
3410
|
-
}
|
|
3411
|
-
function isStaticMemberExpression(node) {
|
|
3412
|
-
return node.type === "MemberExpression" && "computed" in node && !node.computed;
|
|
3413
|
-
}
|
|
3414
|
-
function getCalleeName(callee) {
|
|
3415
|
-
if (isIdentifier(callee)) return callee.name;
|
|
3416
|
-
if (isStaticMemberExpression(callee) && isIdentifier(callee.object) && ALL_FUNCTIONS.has(callee.object.name)) return callee.object.name;
|
|
3417
|
-
}
|
|
3418
|
-
function extractDefinition(node, source) {
|
|
3419
|
-
const name = getCalleeName(node.callee);
|
|
3420
|
-
if (name === void 0 || !ALL_FUNCTIONS.has(name)) return;
|
|
3421
|
-
const firstArgument = node.arguments[0];
|
|
3422
|
-
if (firstArgument === void 0 || firstArgument.type === "SpreadElement") return;
|
|
3423
|
-
return {
|
|
3424
|
-
name: extractStringArgument(firstArgument, source),
|
|
3425
|
-
end: node.end,
|
|
3426
|
-
start: node.start,
|
|
3427
|
-
type: TEST_FUNCTIONS.has(name) ? "test" : "suite"
|
|
3428
|
-
};
|
|
3429
|
-
}
|
|
3430
|
-
//#endregion
|
|
3431
|
-
//#region src/typecheck/parse.ts
|
|
3432
|
-
const errorCodeRegExp = /error TS(?<errorCode>\d+)/;
|
|
3433
|
-
function parseTscErrorLine(line) {
|
|
3434
|
-
const parenIndex = line.lastIndexOf("(", line.indexOf("): error TS"));
|
|
3435
|
-
if (parenIndex === -1) return ["", null];
|
|
3436
|
-
const filePath = line.slice(0, parenIndex);
|
|
3437
|
-
const rest = line.slice(parenIndex);
|
|
3438
|
-
const closeParenIndex = rest.indexOf(")");
|
|
3439
|
-
const [lineString, columnString] = rest.slice(1, closeParenIndex).split(",");
|
|
3440
|
-
if (lineString === void 0 || lineString === "" || columnString === void 0 || columnString === "") return [filePath, null];
|
|
3441
|
-
const afterParen = rest.slice(closeParenIndex + 1);
|
|
3442
|
-
const errorCodeString = errorCodeRegExp.exec(afterParen)?.groups?.["errorCode"];
|
|
3443
|
-
if (errorCodeString === void 0) return [filePath, null];
|
|
3444
|
-
const errorCode = Number(errorCodeString);
|
|
3445
|
-
const marker = `error TS${String(errorCode)}: `;
|
|
3446
|
-
const markerIndex = afterParen.indexOf(marker);
|
|
3447
|
-
const errorMessage = afterParen.slice(markerIndex + marker.length).trim();
|
|
3448
|
-
return [filePath, {
|
|
3449
|
-
column: Number(columnString),
|
|
3450
|
-
errorCode,
|
|
3451
|
-
errorMessage,
|
|
3452
|
-
filePath,
|
|
3453
|
-
line: Number(lineString)
|
|
3454
|
-
}];
|
|
3455
|
-
}
|
|
3456
|
-
function parseTscOutput(stdout) {
|
|
3457
|
-
const map = /* @__PURE__ */ new Map();
|
|
3458
|
-
const merged = stdout.split(/\r?\n/).reduce((lines, next) => {
|
|
3459
|
-
if (!next) return lines;
|
|
3460
|
-
if (next[0] !== " ") lines.push(next);
|
|
3461
|
-
else if (lines.length > 0) lines[lines.length - 1] += `\n${next}`;
|
|
3462
|
-
return lines;
|
|
3463
|
-
}, []);
|
|
3464
|
-
for (const line of merged) {
|
|
3465
|
-
const [filePath, info] = parseTscErrorLine(line);
|
|
3466
|
-
if (!info) continue;
|
|
3467
|
-
const existing = map.get(filePath);
|
|
3468
|
-
if (existing) existing.push(info);
|
|
3469
|
-
else map.set(filePath, [info]);
|
|
3470
|
-
}
|
|
3471
|
-
return map;
|
|
3472
|
-
}
|
|
3473
|
-
//#endregion
|
|
3474
|
-
//#region src/typecheck/runner.ts
|
|
3475
|
-
function createLocationsIndexMap(source) {
|
|
3476
|
-
const map = /* @__PURE__ */ new Map();
|
|
3477
|
-
let index = 0;
|
|
3478
|
-
let line = 1;
|
|
3479
|
-
let column = 1;
|
|
3480
|
-
for (const char of source) {
|
|
3481
|
-
map.set(`${String(line)}:${String(column)}`, index);
|
|
3482
|
-
index++;
|
|
3483
|
-
if (char === "\n") {
|
|
3484
|
-
line++;
|
|
3485
|
-
column = 1;
|
|
3486
|
-
} else column++;
|
|
3487
|
-
}
|
|
3488
|
-
return map;
|
|
3489
|
-
}
|
|
3490
|
-
function mapErrorsToTests(errors, files, startTime) {
|
|
3491
|
-
const testResults = [];
|
|
3492
|
-
let numberFailed = 0;
|
|
3493
|
-
let numberPassed = 0;
|
|
3494
|
-
for (const [filePath, fileInfo] of files) {
|
|
3495
|
-
const fileResult = buildFileResult(filePath, fileInfo, errors.get(filePath) ?? []);
|
|
3496
|
-
testResults.push(fileResult);
|
|
3497
|
-
numberFailed += fileResult.numFailingTests;
|
|
3498
|
-
numberPassed += fileResult.numPassingTests;
|
|
3499
|
-
}
|
|
3500
|
-
return {
|
|
3501
|
-
numFailedTests: numberFailed,
|
|
3502
|
-
numPassedTests: numberPassed,
|
|
3503
|
-
numPendingTests: 0,
|
|
3504
|
-
numTotalTests: numberFailed + numberPassed,
|
|
3505
|
-
startTime,
|
|
3506
|
-
success: numberFailed === 0,
|
|
3507
|
-
testResults
|
|
3508
|
-
};
|
|
3509
|
-
}
|
|
3510
|
-
function isCompositeProject(rootDirectory, tsconfig) {
|
|
3511
|
-
const tsconfigPath = tsconfig !== void 0 ? path$1.resolve(rootDirectory, tsconfig) : path$1.join(rootDirectory, "tsconfig.json");
|
|
3512
|
-
try {
|
|
3513
|
-
return parseJSONC(fs$1.readFileSync(tsconfigPath, "utf-8"))["compilerOptions"]?.["composite"] === true;
|
|
3514
|
-
} catch (err) {
|
|
3515
|
-
if (tsconfig !== void 0) {
|
|
3516
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
3517
|
-
process.stderr.write(`Warning: could not read tsconfig "${tsconfigPath}": ${message}\n`);
|
|
3518
|
-
}
|
|
3519
|
-
return false;
|
|
3520
|
-
}
|
|
3521
|
-
}
|
|
3522
|
-
function runTypecheck(options) {
|
|
3523
|
-
const startTime = Date.now();
|
|
3524
|
-
const errors = parseTscOutput(spawnTsgo(options));
|
|
3525
|
-
const files = /* @__PURE__ */ new Map();
|
|
3526
|
-
for (const filePath of options.files) {
|
|
3527
|
-
const source = fs$1.readFileSync(filePath, "utf-8");
|
|
3528
|
-
const definitions = collectTestDefinitions(source);
|
|
3529
|
-
const resolvedPath = path$1.resolve(filePath);
|
|
3530
|
-
const key = normalizeWindowsPath(path$1.relative(options.rootDir, resolvedPath));
|
|
3531
|
-
files.set(key, {
|
|
3532
|
-
definitions,
|
|
3533
|
-
source
|
|
3534
|
-
});
|
|
3535
|
-
}
|
|
3536
|
-
const resolvedErrors = /* @__PURE__ */ new Map();
|
|
3537
|
-
for (const [errorPath, errorList] of errors) {
|
|
3538
|
-
const resolved = path$1.resolve(options.rootDir, errorPath);
|
|
3539
|
-
const key = normalizeWindowsPath(path$1.relative(options.rootDir, resolved));
|
|
3540
|
-
resolvedErrors.set(key, errorList);
|
|
3541
|
-
}
|
|
3542
|
-
return mapErrorsToTests(resolvedErrors, files, startTime);
|
|
3543
|
-
}
|
|
3544
|
-
function buildFileResult(filePath, fileInfo, errors) {
|
|
3545
|
-
const indexMap = createLocationsIndexMap(fileInfo.source);
|
|
3546
|
-
const testDefinitions = fileInfo.definitions.filter((definition) => definition.type === "test");
|
|
3547
|
-
const sortedDefinitions = [...testDefinitions].sort((a, b) => b.start - a.start);
|
|
3548
|
-
const errorsByTest = /* @__PURE__ */ new Map();
|
|
3549
|
-
const fileErrors = [];
|
|
3550
|
-
for (const error of errors) {
|
|
3551
|
-
const charIndex = indexMap.get(`${String(error.line)}:${String(error.column)}`);
|
|
3552
|
-
const definition = charIndex !== void 0 ? sortedDefinitions.find((td) => td.start <= charIndex && td.end >= charIndex) : void 0;
|
|
3553
|
-
const message = `TS${String(error.errorCode)}: ${error.errorMessage}`;
|
|
3554
|
-
if (definition) {
|
|
3555
|
-
const existing = errorsByTest.get(definition.name) ?? [];
|
|
3556
|
-
existing.push(message);
|
|
3557
|
-
errorsByTest.set(definition.name, existing);
|
|
3558
|
-
} else fileErrors.push(message);
|
|
3559
|
-
}
|
|
3560
|
-
const testCases = testDefinitions.map((definition) => {
|
|
3561
|
-
const failures = errorsByTest.get(definition.name) ?? [];
|
|
3562
|
-
return {
|
|
3563
|
-
ancestorTitles: definition.ancestorNames,
|
|
3564
|
-
failureMessages: failures,
|
|
3565
|
-
fullName: [...definition.ancestorNames, definition.name].join(" > "),
|
|
3566
|
-
status: failures.length > 0 ? "failed" : "passed",
|
|
3567
|
-
title: definition.name
|
|
3568
|
-
};
|
|
3569
|
-
});
|
|
3570
|
-
if (fileErrors.length > 0) testCases.unshift({
|
|
3571
|
-
ancestorTitles: [],
|
|
3572
|
-
failureMessages: fileErrors,
|
|
3573
|
-
fullName: "<file-level type error>",
|
|
3574
|
-
status: "failed",
|
|
3575
|
-
title: "<file-level type error>"
|
|
3576
|
-
});
|
|
3577
|
-
const numberFailing = testCases.filter((testCase) => testCase.status === "failed").length;
|
|
3578
|
-
return {
|
|
3579
|
-
numFailingTests: numberFailing,
|
|
3580
|
-
numPassingTests: testCases.length - numberFailing,
|
|
3581
|
-
numPendingTests: 0,
|
|
3582
|
-
testFilePath: filePath,
|
|
3583
|
-
testResults: testCases
|
|
3584
|
-
};
|
|
3585
|
-
}
|
|
3586
|
-
function isExecSyncError(err) {
|
|
3587
|
-
return typeof err === "object" && err !== null && ("stdout" in err || "stderr" in err);
|
|
3588
|
-
}
|
|
3589
|
-
function resolveTsgoScript() {
|
|
3590
|
-
const packageJsonPath = createRequire(import.meta.url).resolve("@typescript/native-preview/package.json");
|
|
3591
|
-
return path$1.join(path$1.dirname(packageJsonPath), "bin", "tsgo.js");
|
|
3592
|
-
}
|
|
3593
|
-
function spawnTsgo(options) {
|
|
3594
|
-
const composite = isCompositeProject(options.rootDir, options.tsconfig);
|
|
3595
|
-
const args = [];
|
|
3596
|
-
if (composite) args.push("--build", "--emitDeclarationOnly");
|
|
3597
|
-
else args.push("--noEmit");
|
|
3598
|
-
args.push("--pretty", "false");
|
|
3599
|
-
if (options.tsconfig !== void 0) {
|
|
3600
|
-
const resolvedTsconfig = path$1.resolve(options.rootDir, options.tsconfig);
|
|
3601
|
-
if (composite) args.push(resolvedTsconfig);
|
|
3602
|
-
else args.push("-p", resolvedTsconfig);
|
|
3603
|
-
}
|
|
3604
|
-
const tsgoScript = resolveTsgoScript();
|
|
3605
|
-
try {
|
|
3606
|
-
return execFileSync(process.execPath, [tsgoScript, ...args], {
|
|
3607
|
-
cwd: options.rootDir,
|
|
3608
|
-
encoding: "utf-8",
|
|
3609
|
-
stdio: [
|
|
3610
|
-
"ignore",
|
|
3611
|
-
"pipe",
|
|
3612
|
-
"pipe"
|
|
3613
|
-
]
|
|
3614
|
-
});
|
|
3615
|
-
} catch (err) {
|
|
3616
|
-
if (!isExecSyncError(err)) throw err;
|
|
3617
|
-
return err.stdout ?? err.stderr ?? "";
|
|
3618
|
-
}
|
|
3619
|
-
}
|
|
3620
|
-
//#endregion
|
|
3621
|
-
//#region src/utils/game-output.ts
|
|
3622
|
-
function formatGameOutputNotice(filePath, entryCount) {
|
|
3623
|
-
if (entryCount === 0) return "";
|
|
3624
|
-
return `Game output (${String(entryCount)} entries) written to ${filePath}`;
|
|
3625
|
-
}
|
|
3626
|
-
function parseGameOutput(raw) {
|
|
3627
|
-
if (raw === void 0) return [];
|
|
3628
|
-
try {
|
|
3629
|
-
const parsed = JSON.parse(raw);
|
|
3630
|
-
if (!Array.isArray(parsed) || parsed.length === 0) return [];
|
|
3631
|
-
return parsed;
|
|
3632
|
-
} catch {
|
|
3633
|
-
return [];
|
|
3634
|
-
}
|
|
3635
|
-
}
|
|
3636
|
-
function writeGameOutput(filePath, entries) {
|
|
3637
|
-
const absolutePath = path$1.resolve(filePath);
|
|
3638
|
-
const directoryPath = path$1.dirname(absolutePath);
|
|
3639
|
-
if (!fs$1.existsSync(directoryPath)) fs$1.mkdirSync(directoryPath, { recursive: true });
|
|
3640
|
-
fs$1.writeFileSync(absolutePath, JSON.stringify(entries, null, 2));
|
|
3641
|
-
}
|
|
3642
|
-
//#endregion
|
|
3643
|
-
export { collectMounts as A, createOpenCloudBackend as B, formatFailure as C, formatTypecheckSummary as D, formatTestSummary as E, loadConfig$1 as F, VALID_BACKENDS as G, generateTestScript as H, resolveConfig as I, isValidBackend as J, defineConfig as K, StudioBackend as L, findInTree as M, pruneAncestors as N, formatBanner as O, resolveNestedProjects as P, parseJestOutput as Q, createStudioBackend as R, formatAgentMultiProject as S, formatResult as T, DEFAULT_CONFIG as U, buildJestArgv as V, ROOT_ONLY_KEYS as W, LuauScriptError as X, hashBuffer as Y, extractJsonFromOutput as Z, resolveTsconfigDirectories as _, formatAnnotations as a, formatJson as b, visitBlock as c, buildProjectJob as d, execute as f, processProjectResult as g, loadCoverageManifest as h, runTypecheck as i, collectPaths as j, combineSourceMappers as k, visitExpression as l, formatExecuteOutput as m, parseGameOutput as n, formatJobSummary as o, executeBackend as p, defineProject as q, writeGameOutput as r, resolveGitHubActionsOptions as s, formatGameOutputNotice as t, visitStatement as u, rojoProjectSchema as v, formatMultiProjectResult as w, writeJsonFile as x, findFormatterOptions as y, OpenCloudBackend as z };
|