@isentinel/jest-roblox 0.1.2 → 0.1.4
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/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +341 -83
- package/dist/{executor-DqZE3wME.d.mts → executor-CNz6_04-.d.mts} +4 -0
- package/dist/{game-output-BMGxhjkE.mjs → game-output-BL7u7qMT.mjs} +72 -16
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/sea/jest-roblox +0 -0
- package/dist/sea-entry.cjs +2534 -2636
- package/package.json +6 -6
package/dist/cli.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as hashBuffer, C as resolveNestedProjects, D as createStudioBackend, F as VALID_BACKENDS, P as ROOT_ONLY_KEYS, R as isValidBackend, S as collectPaths, _ as formatResult, a as formatAnnotations, b as formatBanner, c as execute, d as findFormatterOptions, g as formatMultiProjectResult, i as runTypecheck, k as createOpenCloudBackend, l as formatExecuteOutput, m as formatAgentMultiProject, n as parseGameOutput, o as formatJobSummary, p as writeJsonFile, r as writeGameOutput, s as resolveGitHubActionsOptions, t as formatGameOutputNotice, u as loadCoverageManifest, w as loadConfig$1, x as rojoProjectSchema, y as formatTypecheckSummary, z as LuauScriptError } from "./game-output-BL7u7qMT.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { type } from "arktype";
|
|
4
4
|
import assert from "node:assert";
|
|
5
5
|
import * as fs$1 from "node:fs";
|
|
6
|
-
import fs
|
|
6
|
+
import fs from "node:fs";
|
|
7
7
|
import * as path$1 from "node:path";
|
|
8
|
-
import path
|
|
8
|
+
import path from "node:path";
|
|
9
9
|
import process from "node:process";
|
|
10
10
|
import { parseArgs as parseArgs$1 } from "node:util";
|
|
11
11
|
import { isAgent } from "std-env";
|
|
@@ -14,16 +14,16 @@ import { WebSocketServer } from "ws";
|
|
|
14
14
|
import { loadConfig } from "c12";
|
|
15
15
|
import * as os from "node:os";
|
|
16
16
|
import { Buffer } from "node:buffer";
|
|
17
|
+
import * as cp from "node:child_process";
|
|
17
18
|
import { RojoResolver } from "@roblox-ts/rojo-resolver";
|
|
18
19
|
import { TraceMap, originalPositionFor } from "@jridgewell/trace-mapping";
|
|
19
20
|
import { getTsconfig } from "get-tsconfig";
|
|
20
21
|
import picomatch from "picomatch";
|
|
21
|
-
import * as cp from "node:child_process";
|
|
22
22
|
import istanbulCoverage from "istanbul-lib-coverage";
|
|
23
23
|
import istanbulReport from "istanbul-lib-report";
|
|
24
24
|
import istanbulReports from "istanbul-reports";
|
|
25
25
|
//#region package.json
|
|
26
|
-
var version = "0.1.
|
|
26
|
+
var version = "0.1.4";
|
|
27
27
|
//#endregion
|
|
28
28
|
//#region src/backends/auto.ts
|
|
29
29
|
var StudioWithFallback = class {
|
|
@@ -115,39 +115,95 @@ function stripTsExtension(pattern) {
|
|
|
115
115
|
return pattern.replace(/\.(tsx?|luau?)$/, "");
|
|
116
116
|
}
|
|
117
117
|
//#endregion
|
|
118
|
-
//#region src/
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
118
|
+
//#region src/luau/eval-literals.ts
|
|
119
|
+
/**
|
|
120
|
+
* Evaluate the first return expression in a Lute-stripped AST root block,
|
|
121
|
+
* supporting only literal values (string, boolean, number, nil, table, cast).
|
|
122
|
+
*
|
|
123
|
+
* Accepts `unknown` and narrows safely — no type casts on JSON.parse needed.
|
|
124
|
+
*/
|
|
125
|
+
function evalLuauReturnLiterals(root) {
|
|
126
|
+
if (!isObject(root) || !Array.isArray(root["statements"])) throw new Error("Config file has no return statement");
|
|
127
|
+
const returnStat = root["statements"].find((stat) => isObject(stat) && stat["tag"] === "return");
|
|
128
|
+
if (!isObject(returnStat) || !Array.isArray(returnStat["expressions"])) throw new Error("Config file has no return statement");
|
|
129
|
+
const first = returnStat["expressions"][0];
|
|
130
|
+
if (!isObject(first) || !("node" in first)) throw new Error("Return statement has no expressions");
|
|
131
|
+
return evalExpr(first["node"]);
|
|
132
|
+
}
|
|
133
|
+
function isObject(value) {
|
|
134
|
+
return typeof value === "object" && value !== null;
|
|
135
|
+
}
|
|
136
|
+
function evalExpr(node) {
|
|
137
|
+
if (!isObject(node)) return;
|
|
138
|
+
let current = node;
|
|
139
|
+
while (current["tag"] === "cast" && isObject(current["operand"])) current = current["operand"];
|
|
140
|
+
const { tag } = current;
|
|
141
|
+
if (tag === "boolean" || tag === "number") return current["value"];
|
|
142
|
+
if (tag === "string") return current["text"];
|
|
143
|
+
if (tag === "table" && Array.isArray(current["entries"])) return evalTable(current["entries"]);
|
|
144
|
+
}
|
|
145
|
+
function evalTable(entries) {
|
|
146
|
+
if (entries.length === 0) return {};
|
|
147
|
+
const first = entries[0];
|
|
148
|
+
if (isObject(first) && first["kind"] === "list") return entries.map((entry) => isObject(entry) ? evalExpr(entry["value"]) : void 0);
|
|
149
|
+
const result = {};
|
|
150
|
+
for (const entry of entries) {
|
|
151
|
+
if (!isObject(entry) || entry["kind"] !== "record") continue;
|
|
152
|
+
const { key, value } = entry;
|
|
153
|
+
if (isObject(key) && typeof key["text"] === "string") result[key["text"]] = evalExpr(value);
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
125
156
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
for (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/luau/parse-ast.luau
|
|
159
|
+
var parse_ast_default = "local fs = require(\"@std/fs\")\nlocal json = require(\"@std/json\")\nlocal process = require(\"@std/process\")\nlocal syntax = require(\"@std/syntax\")\n\nlocal rawArgs = process.args\nlocal userArgs: { string } = {}\nlocal pastSeparator = false\n\nfor _, arg in rawArgs do\n if pastSeparator then\n table.insert(userArgs, arg)\n elseif arg == \"--\" then\n pastSeparator = true\n end\nend\n\nlocal luauRoot = userArgs[1]\nif not luauRoot then\n error(\"Usage: lute run parse-ast.luau -- <file.luau | luau-root> [output-dir]\")\nend\n\nluauRoot = string.gsub(luauRoot, \"\\\\\", \"/\")\n\n-- Fields to keep per AST tag (beyond tag/kind/location which are always kept).\n-- Tags shared by stat/expr variants are merged — nil fields are harmless.\nlocal KEEP: { [string]: { string } } = {\n assign = { \"values\", \"variables\" },\n binary = { \"lhsoperand\", \"rhsoperand\" },\n block = { \"statements\" },\n boolean = { \"value\" },\n call = { \"arguments\", \"func\" },\n cast = { \"operand\" },\n compoundassign = { \"value\", \"variable\" },\n conditional = { \"condition\", \"thenblock\", \"elseifs\", \"elseblock\", \"thenexpr\", \"elseexpr\" },\n [\"do\"] = { \"body\" },\n expression = { \"expression\" },\n [\"for\"] = { \"body\", \"from\", \"to\", \"step\" },\n forin = { \"body\", \"values\" },\n [\"function\"] = { \"body\", \"name\", \"func\" },\n global = { \"name\" },\n group = { \"expression\" },\n index = { \"expression\", \"index\" },\n indexname = { \"expression\", \"accessor\", \"index\" },\n instantiate = { \"expr\" },\n interpolatedstring = { \"expressions\" },\n [\"local\"] = { \"values\", \"variables\" },\n localfunction = { \"name\", \"func\" },\n number = { \"value\" },\n [\"repeat\"] = { \"body\", \"condition\" },\n [\"return\"] = { \"expressions\" },\n string = { \"text\" },\n table = { \"entries\" },\n unary = { \"operand\" },\n [\"while\"] = { \"body\", \"condition\" },\n}\n\nlocal function strip(value: any): any\n if type(value) ~= \"table\" then\n return value\n end\n\n -- LuauSpan — has beginline, no tag\n if value.beginline ~= nil then\n return value\n end\n\n -- Token — has text but no tag, reduce to {text}\n if value.text ~= nil and value.tag == nil then\n return { text = value.text }\n end\n\n -- AST node — has tag, keep only allowlisted fields\n if value.tag ~= nil then\n local result = { tag = value.tag, kind = value.kind, location = value.location }\n local fields = KEEP[value.tag :: string]\n if fields then\n for _, field in fields do\n if value[field] ~= nil then\n result[field] = strip(value[field])\n end\n end\n end\n\n return result\n end\n\n -- Other tables (arrays, Pairs, ElseIf structs) — recurse all fields\n local result: any = {}\n for k, v in value do\n result[k] = strip(v)\n end\n\n return result\nend\n\n-- Single-file mode: parse one file, print stripped AST to stdout\nif string.sub(luauRoot, -5) == \".luau\" or string.sub(luauRoot, -4) == \".lua\" then\n local source = fs.readfiletostring(luauRoot)\n local parseResult = syntax.parse(source)\n print(json.serialize(strip(parseResult.root)))\n return\nend\n\nlocal outputDir = userArgs[2]\nif not outputDir then\n error(\"Usage: lute run parse-ast.luau -- <luau-root> <output-dir>\")\nend\n\noutputDir = string.gsub(outputDir, \"\\\\\", \"/\")\n\n-- Discover .luau files recursively, skipping node_modules, dot dirs, spec/test files\nlocal function discoverFiles(directory: string, relativeTo: string, results: { string })\n local entries = fs.listdirectory(directory)\n for _, entry in entries do\n local fullPath = directory .. \"/\" .. entry.name\n if entry.type == \"dir\" then\n if entry.name == \"node_modules\" or entry.name == \".jest-roblox-coverage\" then\n continue\n end\n\n if string.sub(entry.name, 1, 1) == \".\" then\n continue\n end\n\n discoverFiles(fullPath, relativeTo, results)\n elseif\n entry.type == \"file\"\n and (string.sub(entry.name, -5) == \".luau\" or string.sub(entry.name, -4) == \".lua\")\n then\n if\n string.sub(entry.name, -10) == \".spec.luau\"\n or string.sub(entry.name, -10) == \".test.luau\"\n or string.sub(entry.name, -9) == \".spec.lua\"\n or string.sub(entry.name, -9) == \".test.lua\"\n or string.sub(entry.name, -10) == \".snap.luau\"\n or string.sub(entry.name, -9) == \".snap.lua\"\n then\n continue\n end\n\n -- Compute relative path\n local relative = string.sub(fullPath, #relativeTo + 2)\n table.insert(results, relative)\n end\n end\nend\n\nlocal function dirname(filepath: string): string\n local pos = string.find(filepath, \"/[^/]*$\")\n if pos then\n return string.sub(filepath, 1, pos - 1)\n end\n\n return \"\"\nend\n\nlocal files: { string } = {}\ndiscoverFiles(luauRoot, luauRoot, files)\n\n-- Parse, strip, and write per-file AST JSON\nfor _, relativePath in files do\n local fullPath = luauRoot .. \"/\" .. relativePath\n local source = fs.readfiletostring(fullPath)\n local parseResult = syntax.parse(source)\n local stripped = strip(parseResult.root)\n\n local outPath = outputDir .. \"/\" .. relativePath .. \".json\"\n local dir = dirname(outPath)\n if dir ~= \"\" then\n fs.createdirectory(dir, { makeparents = true })\n end\n\n fs.writestringtofile(outPath, json.serialize(stripped))\nend\n\n-- Print file list to stdout (tiny — just paths)\nprint(json.serialize(files :: json.array))\n";
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/config/luau-config-loader.ts
|
|
162
|
+
let cachedTemporaryDirectory$1;
|
|
163
|
+
/**
|
|
164
|
+
* Parse a .luau config file via Lute and evaluate its return expression.
|
|
165
|
+
*/
|
|
166
|
+
function loadLuauConfig(filePath) {
|
|
167
|
+
const temporaryDirectory = getTemporaryDirectory$1();
|
|
168
|
+
const scriptPath = path$1.join(temporaryDirectory, "parse-ast.luau");
|
|
169
|
+
fs$1.writeFileSync(scriptPath, parse_ast_default);
|
|
170
|
+
let stdout;
|
|
171
|
+
try {
|
|
172
|
+
stdout = cp.execFileSync("lute", [
|
|
173
|
+
"run",
|
|
174
|
+
scriptPath,
|
|
175
|
+
"--",
|
|
176
|
+
path$1.resolve(filePath)
|
|
177
|
+
], {
|
|
178
|
+
encoding: "utf-8",
|
|
179
|
+
maxBuffer: 1024 * 1024
|
|
180
|
+
});
|
|
181
|
+
} catch (err) {
|
|
182
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") throw new Error("lute is required to load .luau config files but was not found on PATH");
|
|
183
|
+
throw new Error(`Failed to evaluate Luau config ${filePath}`, { cause: err });
|
|
149
184
|
}
|
|
150
|
-
|
|
185
|
+
let ast;
|
|
186
|
+
try {
|
|
187
|
+
ast = JSON.parse(stdout);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
throw new Error(`Failed to parse AST JSON from Luau config ${filePath}`, { cause: err });
|
|
190
|
+
}
|
|
191
|
+
const result = evalLuauReturnLiterals(ast);
|
|
192
|
+
if (typeof result !== "object" || result === null) throw new Error(`Luau config ${filePath} must return a table`);
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Check if `<cwd>/<directoryOrFile>/jest.config.luau` exists. Returns the
|
|
197
|
+
* resolved path if found, undefined otherwise.
|
|
198
|
+
*/
|
|
199
|
+
function findLuauConfigFile(directoryOrFile, cwd) {
|
|
200
|
+
const resolved = path$1.resolve(cwd, directoryOrFile, "jest.config.luau");
|
|
201
|
+
if (fs$1.existsSync(resolved)) return resolved;
|
|
202
|
+
}
|
|
203
|
+
function getTemporaryDirectory$1() {
|
|
204
|
+
if (cachedTemporaryDirectory$1 !== void 0 && fs$1.existsSync(cachedTemporaryDirectory$1)) return cachedTemporaryDirectory$1;
|
|
205
|
+
cachedTemporaryDirectory$1 = fs$1.mkdtempSync(path$1.join(os.tmpdir(), "jest-roblox-luau-config-"));
|
|
206
|
+
return cachedTemporaryDirectory$1;
|
|
151
207
|
}
|
|
152
208
|
//#endregion
|
|
153
209
|
//#region src/config/projects.ts
|
|
@@ -249,6 +305,8 @@ function resolveProjectConfig(project, rootConfig, rojoTree) {
|
|
|
249
305
|
};
|
|
250
306
|
}
|
|
251
307
|
async function loadProjectConfigFile(filePath, cwd) {
|
|
308
|
+
const luauConfigPath = findLuauConfigFile(filePath, cwd);
|
|
309
|
+
if (luauConfigPath !== void 0) return buildProjectConfigFromLuau(luauConfigPath, filePath);
|
|
252
310
|
let result;
|
|
253
311
|
try {
|
|
254
312
|
result = await loadConfig({
|
|
@@ -290,6 +348,38 @@ function mergeProjectConfig(rootConfig, project) {
|
|
|
290
348
|
for (const [key, value] of Object.entries(project)) if (!PROJECT_ONLY_KEYS.has(key) && value !== void 0) merged[key] = value;
|
|
291
349
|
return merged;
|
|
292
350
|
}
|
|
351
|
+
const LUAU_BOOLEAN_KEYS = [
|
|
352
|
+
"automock",
|
|
353
|
+
"clearMocks",
|
|
354
|
+
"injectGlobals",
|
|
355
|
+
"mockDataModel",
|
|
356
|
+
"resetMocks",
|
|
357
|
+
"resetModules",
|
|
358
|
+
"restoreMocks"
|
|
359
|
+
];
|
|
360
|
+
const LUAU_NUMBER_KEYS = ["slowTestThreshold", "testTimeout"];
|
|
361
|
+
const LUAU_STRING_KEYS = ["testEnvironment"];
|
|
362
|
+
const LUAU_STRING_ARRAY_KEYS = ["setupFiles", "setupFilesAfterEnv"];
|
|
363
|
+
function copyLuauOptionalFields(raw, config) {
|
|
364
|
+
const record = config;
|
|
365
|
+
for (const key of LUAU_BOOLEAN_KEYS) if (typeof raw[key] === "boolean") record[key] = raw[key];
|
|
366
|
+
for (const key of LUAU_NUMBER_KEYS) if (typeof raw[key] === "number") record[key] = raw[key];
|
|
367
|
+
for (const key of LUAU_STRING_KEYS) if (typeof raw[key] === "string") record[key] = raw[key];
|
|
368
|
+
for (const key of LUAU_STRING_ARRAY_KEYS) if (Array.isArray(raw[key])) record[key] = raw[key];
|
|
369
|
+
}
|
|
370
|
+
function buildProjectConfigFromLuau(luauConfigPath, directoryPath) {
|
|
371
|
+
const raw = loadLuauConfig(luauConfigPath);
|
|
372
|
+
const { displayName } = raw;
|
|
373
|
+
if (typeof displayName !== "string" || displayName === "") throw new Error(`Luau config file "${luauConfigPath}" must have a displayName string`);
|
|
374
|
+
const testMatch = Array.isArray(raw["testMatch"]) ? raw["testMatch"] : void 0;
|
|
375
|
+
const config = {
|
|
376
|
+
displayName,
|
|
377
|
+
include: testMatch !== void 0 ? testMatch.map((pattern) => path$1.posix.join(directoryPath, `${pattern}.luau`)) : [path$1.posix.join(directoryPath, "**/*.spec.luau")]
|
|
378
|
+
};
|
|
379
|
+
if (testMatch !== void 0) config.testMatch = testMatch;
|
|
380
|
+
copyLuauOptionalFields(raw, config);
|
|
381
|
+
return config;
|
|
382
|
+
}
|
|
293
383
|
function matchNodePath(childNode, targetPath, childDataModelPath) {
|
|
294
384
|
const nodePath = childNode.$path;
|
|
295
385
|
if (typeof nodePath !== "string") return;
|
|
@@ -365,6 +455,8 @@ function serializeToLuau(config) {
|
|
|
365
455
|
}
|
|
366
456
|
function generateProjectConfigs(projects) {
|
|
367
457
|
for (const project of projects) {
|
|
458
|
+
const luauConfigPath = project.outputPath.replace(/\.lua$/, ".luau");
|
|
459
|
+
if (fs.existsSync(luauConfigPath)) continue;
|
|
368
460
|
const content = serializeToLuau(project.config);
|
|
369
461
|
const directory = path.dirname(project.outputPath);
|
|
370
462
|
fs.mkdirSync(directory, { recursive: true });
|
|
@@ -470,27 +562,133 @@ function mapCoverageToTypeScript(coverageData, manifest) {
|
|
|
470
562
|
if (record === void 0) continue;
|
|
471
563
|
const resources = loadFileResources(record);
|
|
472
564
|
if (resources === void 0) continue;
|
|
473
|
-
|
|
474
|
-
|
|
565
|
+
if (resources.traceMap === void 0) {
|
|
566
|
+
passthroughFileStatements(resources, fileCoverage, pendingStatements);
|
|
567
|
+
passthroughFileFunctions(resources, fileCoverage, pendingFunctions);
|
|
568
|
+
passthroughFileBranches(resources, fileCoverage, pendingBranches);
|
|
569
|
+
} else {
|
|
570
|
+
const mapped = {
|
|
571
|
+
coverageMap: resources.coverageMap,
|
|
572
|
+
traceMap: resources.traceMap
|
|
573
|
+
};
|
|
574
|
+
mapFileFunctions(mapped, fileCoverage, pendingFunctions, mapFileStatements(mapped, fileCoverage, pendingStatements));
|
|
575
|
+
mapFileBranches(mapped, fileCoverage, pendingBranches);
|
|
576
|
+
}
|
|
475
577
|
}
|
|
476
578
|
return buildResult(pendingStatements, pendingFunctions, pendingBranches);
|
|
477
579
|
}
|
|
478
580
|
function loadFileResources(record) {
|
|
479
581
|
let coverageMapRaw;
|
|
480
|
-
let sourceMapRaw;
|
|
481
582
|
try {
|
|
482
583
|
coverageMapRaw = fs$1.readFileSync(record.coverageMapPath, "utf-8");
|
|
483
|
-
sourceMapRaw = fs$1.readFileSync(record.sourceMapPath, "utf-8");
|
|
484
584
|
} catch {
|
|
485
585
|
return;
|
|
486
586
|
}
|
|
487
587
|
const parsed = coverageMapSchema(JSON.parse(coverageMapRaw));
|
|
488
588
|
if (parsed instanceof type.errors) return;
|
|
589
|
+
let traceMap;
|
|
590
|
+
try {
|
|
591
|
+
traceMap = new TraceMap(fs$1.readFileSync(record.sourceMapPath, "utf-8"));
|
|
592
|
+
} catch {}
|
|
489
593
|
return {
|
|
490
594
|
coverageMap: parsed,
|
|
491
|
-
|
|
595
|
+
sourceKey: record.key,
|
|
596
|
+
traceMap
|
|
492
597
|
};
|
|
493
598
|
}
|
|
599
|
+
function toIstanbulColumn(luauColumn) {
|
|
600
|
+
return Math.max(0, luauColumn - 1);
|
|
601
|
+
}
|
|
602
|
+
function passthroughFileStatements(resources, fileCoverage, pending) {
|
|
603
|
+
let fileStatements = pending.get(resources.sourceKey);
|
|
604
|
+
if (fileStatements === void 0) {
|
|
605
|
+
fileStatements = /* @__PURE__ */ new Map();
|
|
606
|
+
pending.set(resources.sourceKey, fileStatements);
|
|
607
|
+
}
|
|
608
|
+
for (const [statementId, rawSpan] of Object.entries(resources.coverageMap.statementMap)) {
|
|
609
|
+
const span = spanSchema(rawSpan);
|
|
610
|
+
if (span instanceof type.errors) continue;
|
|
611
|
+
const hitCount = fileCoverage.s[statementId] ?? 0;
|
|
612
|
+
fileStatements.set(statementId, {
|
|
613
|
+
end: {
|
|
614
|
+
column: toIstanbulColumn(span.end.column),
|
|
615
|
+
line: span.end.line
|
|
616
|
+
},
|
|
617
|
+
hitCount,
|
|
618
|
+
start: {
|
|
619
|
+
column: toIstanbulColumn(span.start.column),
|
|
620
|
+
line: span.start.line
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
function passthroughFileFunctions(resources, fileCoverage, pendingFunctions) {
|
|
626
|
+
if (resources.coverageMap.functionMap === void 0) return;
|
|
627
|
+
let fileFunctions = pendingFunctions.get(resources.sourceKey);
|
|
628
|
+
if (fileFunctions === void 0) {
|
|
629
|
+
fileFunctions = [];
|
|
630
|
+
pendingFunctions.set(resources.sourceKey, fileFunctions);
|
|
631
|
+
}
|
|
632
|
+
for (const [functionId, rawEntry] of Object.entries(resources.coverageMap.functionMap)) {
|
|
633
|
+
const entry = functionEntrySchema(rawEntry);
|
|
634
|
+
if (entry instanceof type.errors) continue;
|
|
635
|
+
fileFunctions.push({
|
|
636
|
+
name: entry.name,
|
|
637
|
+
hitCount: fileCoverage.f?.[functionId] ?? 0,
|
|
638
|
+
loc: {
|
|
639
|
+
end: {
|
|
640
|
+
column: toIstanbulColumn(entry.location.end.column),
|
|
641
|
+
line: entry.location.end.line
|
|
642
|
+
},
|
|
643
|
+
start: {
|
|
644
|
+
column: toIstanbulColumn(entry.location.start.column),
|
|
645
|
+
line: entry.location.start.line
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function passthroughFileBranches(resources, fileCoverage, pendingBranches) {
|
|
652
|
+
if (resources.coverageMap.branchMap === void 0) return;
|
|
653
|
+
let fileBranches = pendingBranches.get(resources.sourceKey);
|
|
654
|
+
if (fileBranches === void 0) {
|
|
655
|
+
fileBranches = [];
|
|
656
|
+
pendingBranches.set(resources.sourceKey, fileBranches);
|
|
657
|
+
}
|
|
658
|
+
for (const [branchId, rawEntry] of Object.entries(resources.coverageMap.branchMap)) {
|
|
659
|
+
const entry = branchEntrySchema(rawEntry);
|
|
660
|
+
if (entry instanceof type.errors) continue;
|
|
661
|
+
const armHitCounts = fileCoverage.b?.[branchId] ?? [];
|
|
662
|
+
const locations = [];
|
|
663
|
+
for (const rawLocation of entry.locations) {
|
|
664
|
+
const location = spanSchema(rawLocation);
|
|
665
|
+
if (location instanceof type.errors) continue;
|
|
666
|
+
locations.push({
|
|
667
|
+
end: {
|
|
668
|
+
column: toIstanbulColumn(location.end.column),
|
|
669
|
+
line: location.end.line
|
|
670
|
+
},
|
|
671
|
+
start: {
|
|
672
|
+
column: toIstanbulColumn(location.start.column),
|
|
673
|
+
line: location.start.line
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
if (locations.length === 0) continue;
|
|
678
|
+
const firstLocation = locations[0];
|
|
679
|
+
const lastLocation = locations[locations.length - 1];
|
|
680
|
+
assert(firstLocation !== void 0 && lastLocation !== void 0, "Branch locations must not be empty after filtering");
|
|
681
|
+
fileBranches.push({
|
|
682
|
+
armHitCounts: entry.locations.map((_, index) => armHitCounts[index] ?? 0),
|
|
683
|
+
loc: {
|
|
684
|
+
end: lastLocation.end,
|
|
685
|
+
start: firstLocation.start
|
|
686
|
+
},
|
|
687
|
+
locations,
|
|
688
|
+
type: entry.type
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
}
|
|
494
692
|
function mapStatement(traceMap, span) {
|
|
495
693
|
const mappedStart = originalPositionFor(traceMap, {
|
|
496
694
|
column: Math.max(0, span.start.column - 1),
|
|
@@ -731,26 +929,47 @@ function buildResult(pending, pendingFunctions, pendingBranches) {
|
|
|
731
929
|
return { files };
|
|
732
930
|
}
|
|
733
931
|
//#endregion
|
|
734
|
-
//#region src/
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
]);
|
|
932
|
+
//#region src/coverage/merge-raw-coverage.ts
|
|
933
|
+
/**
|
|
934
|
+
* Additively merge two raw coverage datasets. Overlapping files have their
|
|
935
|
+
* hit counts summed (matching istanbul-lib-coverage's semantics).
|
|
936
|
+
*/
|
|
937
|
+
function mergeRawCoverage(target, source) {
|
|
938
|
+
if (target === void 0) return source;
|
|
939
|
+
if (source === void 0) return target;
|
|
940
|
+
const result = { ...target };
|
|
941
|
+
for (const [filePath, fileCoverage] of Object.entries(source)) {
|
|
942
|
+
const existing = result[filePath];
|
|
943
|
+
result[filePath] = existing === void 0 ? { ...fileCoverage } : mergeFileCoverage(existing, fileCoverage);
|
|
944
|
+
}
|
|
945
|
+
return result;
|
|
946
|
+
}
|
|
947
|
+
function sumScalars(a, b) {
|
|
948
|
+
const result = { ...a };
|
|
949
|
+
for (const [key, value] of Object.entries(b)) result[key] = (result[key] ?? 0) + value;
|
|
950
|
+
return result;
|
|
951
|
+
}
|
|
952
|
+
function sumBranches(a, b) {
|
|
953
|
+
const result = { ...a };
|
|
954
|
+
for (const [key, bArms] of Object.entries(b)) {
|
|
955
|
+
const aArms = result[key];
|
|
956
|
+
if (aArms === void 0) {
|
|
957
|
+
result[key] = [...bArms];
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
const length = Math.max(aArms.length, bArms.length);
|
|
961
|
+
result[key] = Array.from({ length }, (_, index) => (aArms[index] ?? 0) + (bArms[index] ?? 0));
|
|
962
|
+
}
|
|
963
|
+
return result;
|
|
964
|
+
}
|
|
965
|
+
function mergeFileCoverage(a, b) {
|
|
966
|
+
const merged = { s: sumScalars(a.s, b.s) };
|
|
967
|
+
if (a.f !== void 0 || b.f !== void 0) merged.f = sumScalars(a.f ?? {}, b.f ?? {});
|
|
968
|
+
if (a.b !== void 0 || b.b !== void 0) merged.b = sumBranches(a.b ?? {}, b.b ?? {});
|
|
969
|
+
return merged;
|
|
970
|
+
}
|
|
752
971
|
//#endregion
|
|
753
|
-
//#region src/
|
|
972
|
+
//#region src/luau/visitor.ts
|
|
754
973
|
function visitExpression(expression, visitor) {
|
|
755
974
|
if (visitor.visitExpr?.(expression) === false) return;
|
|
756
975
|
const { tag } = expression;
|
|
@@ -1021,6 +1240,23 @@ function visitExprInstantiate(node, visitor) {
|
|
|
1021
1240
|
}
|
|
1022
1241
|
//#endregion
|
|
1023
1242
|
//#region src/coverage/coverage-collector.ts
|
|
1243
|
+
const INSTRUMENTABLE_STATEMENT_TAGS = new Set([
|
|
1244
|
+
"assign",
|
|
1245
|
+
"break",
|
|
1246
|
+
"compoundassign",
|
|
1247
|
+
"conditional",
|
|
1248
|
+
"continue",
|
|
1249
|
+
"do",
|
|
1250
|
+
"expression",
|
|
1251
|
+
"for",
|
|
1252
|
+
"forin",
|
|
1253
|
+
"function",
|
|
1254
|
+
"local",
|
|
1255
|
+
"localfunction",
|
|
1256
|
+
"repeat",
|
|
1257
|
+
"return",
|
|
1258
|
+
"while"
|
|
1259
|
+
]);
|
|
1024
1260
|
const END_KEYWORD_LENGTH = 3;
|
|
1025
1261
|
function collectCoverage(root) {
|
|
1026
1262
|
let statementIndex = 1;
|
|
@@ -1260,9 +1496,6 @@ function buildCoverageMap$1(result) {
|
|
|
1260
1496
|
};
|
|
1261
1497
|
}
|
|
1262
1498
|
//#endregion
|
|
1263
|
-
//#region src/coverage/parse-ast.luau
|
|
1264
|
-
var parse_ast_default = "local fs = require(\"@std/fs\")\nlocal json = require(\"@std/json\")\nlocal process = require(\"@std/process\")\nlocal syntax = require(\"@std/syntax\")\n\nlocal rawArgs = process.args\nlocal userArgs: { string } = {}\nlocal pastSeparator = false\n\nfor _, arg in rawArgs do\n if pastSeparator then\n table.insert(userArgs, arg)\n elseif arg == \"--\" then\n pastSeparator = true\n end\nend\n\nlocal luauRoot = userArgs[1]\nlocal outputDir = userArgs[2]\nif not luauRoot or not outputDir then\n error(\"Usage: lute run parse-ast.luau -- <luau-root> <output-dir>\")\nend\n\nluauRoot = string.gsub(luauRoot, \"\\\\\", \"/\")\noutputDir = string.gsub(outputDir, \"\\\\\", \"/\")\n\n-- Fields to keep per AST tag (beyond tag/kind/location which are always kept).\n-- Tags shared by stat/expr variants are merged — nil fields are harmless.\nlocal KEEP: { [string]: { string } } = {\n assign = { \"values\", \"variables\" },\n binary = { \"lhsoperand\", \"rhsoperand\" },\n block = { \"statements\" },\n call = { \"arguments\", \"func\" },\n cast = { \"operand\" },\n compoundassign = { \"value\", \"variable\" },\n conditional = { \"condition\", \"thenblock\", \"elseifs\", \"elseblock\", \"thenexpr\", \"elseexpr\" },\n [\"do\"] = { \"body\" },\n expression = { \"expression\" },\n [\"for\"] = { \"body\", \"from\", \"to\", \"step\" },\n forin = { \"body\", \"values\" },\n [\"function\"] = { \"body\", \"name\", \"func\" },\n global = { \"name\" },\n group = { \"expression\" },\n index = { \"expression\", \"index\" },\n indexname = { \"expression\", \"accessor\", \"index\" },\n instantiate = { \"expr\" },\n interpolatedstring = { \"expressions\" },\n [\"local\"] = { \"values\", \"variables\" },\n localfunction = { \"name\", \"func\" },\n [\"repeat\"] = { \"body\", \"condition\" },\n [\"return\"] = { \"expressions\" },\n table = { \"entries\" },\n unary = { \"operand\" },\n [\"while\"] = { \"body\", \"condition\" },\n}\n\nlocal function strip(value: any): any\n if type(value) ~= \"table\" then\n return value\n end\n\n -- LuauSpan — has beginline, no tag\n if value.beginline ~= nil then\n return value\n end\n\n -- Token — has text but no tag, reduce to {text}\n if value.text ~= nil and value.tag == nil then\n return { text = value.text }\n end\n\n -- AST node — has tag, keep only allowlisted fields\n if value.tag ~= nil then\n local result = { tag = value.tag, kind = value.kind, location = value.location }\n local fields = KEEP[value.tag :: string]\n if fields then\n for _, field in fields do\n if value[field] ~= nil then\n result[field] = strip(value[field])\n end\n end\n end\n\n return result\n end\n\n -- Other tables (arrays, Pairs, ElseIf structs) — recurse all fields\n local result: any = {}\n for k, v in value do\n result[k] = strip(v)\n end\n\n return result\nend\n\n-- Discover .luau files recursively, skipping node_modules, dot dirs, spec/test files\nlocal function discoverFiles(directory: string, relativeTo: string, results: { string })\n local entries = fs.listdirectory(directory)\n for _, entry in entries do\n local fullPath = directory .. \"/\" .. entry.name\n if entry.type == \"dir\" then\n if entry.name == \"node_modules\" or entry.name == \".jest-roblox-coverage\" then\n continue\n end\n\n if string.sub(entry.name, 1, 1) == \".\" then\n continue\n end\n\n discoverFiles(fullPath, relativeTo, results)\n elseif\n entry.type == \"file\"\n and (string.sub(entry.name, -5) == \".luau\" or string.sub(entry.name, -4) == \".lua\")\n then\n if\n string.sub(entry.name, -10) == \".spec.luau\"\n or string.sub(entry.name, -10) == \".test.luau\"\n or string.sub(entry.name, -9) == \".spec.lua\"\n or string.sub(entry.name, -9) == \".test.lua\"\n or string.sub(entry.name, -10) == \".snap.luau\"\n or string.sub(entry.name, -9) == \".snap.lua\"\n then\n continue\n end\n\n -- Compute relative path\n local relative = string.sub(fullPath, #relativeTo + 2)\n table.insert(results, relative)\n end\n end\nend\n\nlocal function dirname(filepath: string): string\n local pos = string.find(filepath, \"/[^/]*$\")\n if pos then\n return string.sub(filepath, 1, pos - 1)\n end\n\n return \"\"\nend\n\nlocal files: { string } = {}\ndiscoverFiles(luauRoot, luauRoot, files)\n\n-- Parse, strip, and write per-file AST JSON\nfor _, relativePath in files do\n local fullPath = luauRoot .. \"/\" .. relativePath\n local source = fs.readfiletostring(fullPath)\n local parseResult = syntax.parse(source)\n local stripped = strip(parseResult.root)\n\n local outPath = outputDir .. \"/\" .. relativePath .. \".json\"\n local dir = dirname(outPath)\n if dir ~= \"\" then\n fs.createdirectory(dir, { makeparents = true })\n end\n\n fs.writestringtofile(outPath, json.serialize(stripped))\nend\n\n-- Print file list to stdout (tiny — just paths)\nprint(json.serialize(files :: json.array))\n";
|
|
1265
|
-
//#endregion
|
|
1266
1499
|
//#region src/coverage/probe-inserter.ts
|
|
1267
1500
|
function insertProbes(source, result, fileKey) {
|
|
1268
1501
|
const lines = splitLines(source);
|
|
@@ -1317,7 +1550,7 @@ function applyProbes(mutableLines, probes) {
|
|
|
1317
1550
|
assert(line !== void 0, `Invalid probe line number: ${probeLine}`);
|
|
1318
1551
|
const before = line.slice(0, column - 1);
|
|
1319
1552
|
const after = line.slice(column - 1);
|
|
1320
|
-
mutableLines[lineIndex] = before + text + after;
|
|
1553
|
+
mutableLines[lineIndex] = before + (before.length > 0 && !/\s$/.test(before) && /^[a-zA-Z_]/.test(text) ? " " : "") + text + after;
|
|
1321
1554
|
}
|
|
1322
1555
|
}
|
|
1323
1556
|
function extractModeDirective(lines) {
|
|
@@ -1743,7 +1976,7 @@ function printCoverageHeader() {
|
|
|
1743
1976
|
process.stdout.write(`\n${header}\n`);
|
|
1744
1977
|
}
|
|
1745
1978
|
function generateReports(options) {
|
|
1746
|
-
const coverageMap = buildCoverageMap(options.mapped);
|
|
1979
|
+
const coverageMap = buildCoverageMap(filterMappedFiles(options.mapped, options.collectCoverageFrom));
|
|
1747
1980
|
const context = istanbulReport.createContext({
|
|
1748
1981
|
coverageMap,
|
|
1749
1982
|
dir: options.coverageDirectory
|
|
@@ -1753,8 +1986,8 @@ function generateReports(options) {
|
|
|
1753
1986
|
istanbulReports.create(reporterName).execute(context);
|
|
1754
1987
|
}
|
|
1755
1988
|
}
|
|
1756
|
-
function checkThresholds(mapped, thresholds) {
|
|
1757
|
-
const summary = buildCoverageMap(mapped).getCoverageSummary();
|
|
1989
|
+
function checkThresholds(mapped, thresholds, collectCoverageFrom) {
|
|
1990
|
+
const summary = buildCoverageMap(filterMappedFiles(mapped, collectCoverageFrom)).getCoverageSummary();
|
|
1758
1991
|
const failures = [];
|
|
1759
1992
|
const checks = [
|
|
1760
1993
|
{
|
|
@@ -1821,6 +2054,26 @@ function buildCoverageMap(mapped) {
|
|
|
1821
2054
|
}
|
|
1822
2055
|
return coverageMap;
|
|
1823
2056
|
}
|
|
2057
|
+
function createGlobMatcher(patterns) {
|
|
2058
|
+
const withPath = patterns.filter((pattern) => pattern.includes("/"));
|
|
2059
|
+
const withoutPath = patterns.filter((pattern) => !pattern.includes("/"));
|
|
2060
|
+
const matchers = [];
|
|
2061
|
+
if (withPath.length > 0) matchers.push(picomatch(withPath));
|
|
2062
|
+
if (withoutPath.length > 0) matchers.push(picomatch(withoutPath, { matchBase: true }));
|
|
2063
|
+
return (filePath) => matchers.some((matcher) => matcher(filePath));
|
|
2064
|
+
}
|
|
2065
|
+
function filterMappedFiles(mapped, collectCoverageFrom) {
|
|
2066
|
+
if (collectCoverageFrom === void 0 || collectCoverageFrom.length === 0) return mapped;
|
|
2067
|
+
const includePatterns = collectCoverageFrom.filter((pattern) => !pattern.startsWith("!"));
|
|
2068
|
+
const excludePatterns = collectCoverageFrom.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
|
|
2069
|
+
const isIncluded = includePatterns.length > 0 ? createGlobMatcher(includePatterns) : () => true;
|
|
2070
|
+
const isExcluded = excludePatterns.length > 0 ? createGlobMatcher(excludePatterns) : () => false;
|
|
2071
|
+
const cwd = process.cwd();
|
|
2072
|
+
return { files: Object.fromEntries(Object.entries(mapped.files).filter(([filePath]) => {
|
|
2073
|
+
const relativePath = path$1.isAbsolute(filePath) ? path$1.relative(cwd, filePath).replaceAll("\\", "/") : filePath;
|
|
2074
|
+
return isIncluded(relativePath) && !isExcluded(relativePath);
|
|
2075
|
+
})) };
|
|
2076
|
+
}
|
|
1824
2077
|
function isValidReporter(name) {
|
|
1825
2078
|
return VALID_REPORTERS.has(name);
|
|
1826
2079
|
}
|
|
@@ -1866,11 +2119,13 @@ Options:
|
|
|
1866
2119
|
--gameOutput <path> Write game output (print/warn/error) to file
|
|
1867
2120
|
--sourceMap Map Luau stack traces to TypeScript source
|
|
1868
2121
|
--rojoProject <path> Path to rojo project file (auto-detected if not set)
|
|
2122
|
+
--passWithNoTests Exit with 0 when no test files are found
|
|
1869
2123
|
--verbose Show individual test results
|
|
1870
2124
|
--silent Suppress output
|
|
1871
2125
|
--no-color Disable colored output
|
|
1872
2126
|
-u, --updateSnapshot Update snapshot files
|
|
1873
2127
|
--coverage Enable coverage collection
|
|
2128
|
+
--collectCoverageFrom <glob> Globs for files to include in coverage (repeatable)
|
|
1874
2129
|
--coverageDirectory <path> Directory for coverage output (default: coverage)
|
|
1875
2130
|
--coverageReporters <r...> Coverage reporters (default: text, lcov)
|
|
1876
2131
|
--formatters <name...> Output formatters (default, agent, json, github-actions)
|
|
@@ -1906,6 +2161,10 @@ function parseArgs(args) {
|
|
|
1906
2161
|
options: {
|
|
1907
2162
|
"backend": { type: "string" },
|
|
1908
2163
|
"cache": { type: "boolean" },
|
|
2164
|
+
"collectCoverageFrom": {
|
|
2165
|
+
multiple: true,
|
|
2166
|
+
type: "string"
|
|
2167
|
+
},
|
|
1909
2168
|
"color": { type: "boolean" },
|
|
1910
2169
|
"config": { type: "string" },
|
|
1911
2170
|
"coverage": { type: "boolean" },
|
|
@@ -1927,6 +2186,7 @@ function parseArgs(args) {
|
|
|
1927
2186
|
"no-color": { type: "boolean" },
|
|
1928
2187
|
"no-show-luau": { type: "boolean" },
|
|
1929
2188
|
"outputFile": { type: "string" },
|
|
2189
|
+
"passWithNoTests": { type: "boolean" },
|
|
1930
2190
|
"pollInterval": { type: "string" },
|
|
1931
2191
|
"port": { type: "string" },
|
|
1932
2192
|
"project": {
|
|
@@ -1973,6 +2233,7 @@ function parseArgs(args) {
|
|
|
1973
2233
|
backend: validateBackend(values.backend),
|
|
1974
2234
|
cache: values["no-cache"] === true ? false : values.cache,
|
|
1975
2235
|
collectCoverage: values.coverage,
|
|
2236
|
+
collectCoverageFrom: values.collectCoverageFrom,
|
|
1976
2237
|
color: values["no-color"] === true ? false : values.color,
|
|
1977
2238
|
config: values.config,
|
|
1978
2239
|
coverageDirectory: values.coverageDirectory,
|
|
@@ -1982,6 +2243,7 @@ function parseArgs(args) {
|
|
|
1982
2243
|
gameOutput: values.gameOutput,
|
|
1983
2244
|
help: values.help,
|
|
1984
2245
|
outputFile: values.outputFile,
|
|
2246
|
+
passWithNoTests: values.passWithNoTests,
|
|
1985
2247
|
pollInterval,
|
|
1986
2248
|
port,
|
|
1987
2249
|
project: values.project,
|
|
@@ -2043,10 +2305,7 @@ function mergeProjectResults(results) {
|
|
|
2043
2305
|
totalMs += result.timing.totalMs;
|
|
2044
2306
|
uploadMs += result.timing.uploadMs ?? 0;
|
|
2045
2307
|
coverageMs += result.timing.coverageMs ?? 0;
|
|
2046
|
-
if (result.coverageData !== void 0) mergedCoverage =
|
|
2047
|
-
...mergedCoverage,
|
|
2048
|
-
...result.coverageData
|
|
2049
|
-
};
|
|
2308
|
+
if (result.coverageData !== void 0) mergedCoverage = mergeRawCoverage(mergedCoverage, result.coverageData);
|
|
2050
2309
|
}
|
|
2051
2310
|
return {
|
|
2052
2311
|
coverageData: mergedCoverage,
|
|
@@ -2142,12 +2401,13 @@ function processCoverage(config, coverageData) {
|
|
|
2142
2401
|
const coverageDirectory = path$1.resolve(config.rootDir, config.coverageDirectory);
|
|
2143
2402
|
if (!config.silent) printCoverageHeader();
|
|
2144
2403
|
generateReports({
|
|
2404
|
+
collectCoverageFrom: config.collectCoverageFrom,
|
|
2145
2405
|
coverageDirectory,
|
|
2146
2406
|
mapped,
|
|
2147
2407
|
reporters: config.coverageReporters
|
|
2148
2408
|
});
|
|
2149
2409
|
if (config.coverageThreshold !== void 0) {
|
|
2150
|
-
const result = checkThresholds(mapped, config.coverageThreshold);
|
|
2410
|
+
const result = checkThresholds(mapped, config.coverageThreshold, config.collectCoverageFrom);
|
|
2151
2411
|
if (!result.passed) {
|
|
2152
2412
|
for (const failure of result.failures) process.stderr.write(`Coverage threshold not met for ${failure.metric}: ${String(failure.actual.toFixed(2))}% < ${String(failure.threshold)}%\n`);
|
|
2153
2413
|
return false;
|
|
@@ -2177,7 +2437,7 @@ function runGitHubActionsFormatter(config, result, sourceMapper) {
|
|
|
2177
2437
|
function hasFormatter(config, name) {
|
|
2178
2438
|
return config.formatters?.some((entry) => Array.isArray(entry) ? entry[0] === name : entry === name) === true;
|
|
2179
2439
|
}
|
|
2180
|
-
function
|
|
2440
|
+
function getAgentMaxFailures(config) {
|
|
2181
2441
|
assert(config.formatters !== void 0, "formatters is set by resolveFormatters");
|
|
2182
2442
|
const options = findFormatterOptions(config.formatters, "agent");
|
|
2183
2443
|
if (options !== void 0 && typeof options["maxFailures"] === "number") return options["maxFailures"];
|
|
@@ -2264,9 +2524,9 @@ function printMultiProjectOutput(options) {
|
|
|
2264
2524
|
const { config, merged, preCoverageMs, projectResults, typecheckResult } = options;
|
|
2265
2525
|
const timing = addCoverageTiming(merged.timing, preCoverageMs);
|
|
2266
2526
|
if (usesAgentFormatter(config)) {
|
|
2267
|
-
printOutput(
|
|
2527
|
+
printOutput(formatAgentMultiProject(toProjectEntries(projectResults), {
|
|
2268
2528
|
gameOutput: config.gameOutput,
|
|
2269
|
-
maxFailures:
|
|
2529
|
+
maxFailures: getAgentMaxFailures(config),
|
|
2270
2530
|
outputFile: config.outputFile,
|
|
2271
2531
|
rootDir: config.rootDir,
|
|
2272
2532
|
sourceMapper: merged.sourceMapper,
|
|
@@ -2423,6 +2683,7 @@ async function runMultiProject(cli, rootConfig, projectEntries) {
|
|
|
2423
2683
|
tsconfig: rootConfig.typecheckTsconfig
|
|
2424
2684
|
}) : void 0;
|
|
2425
2685
|
if (projectResults.length === 0 && typecheckResult === void 0) {
|
|
2686
|
+
if (rootConfig.passWithNoTests) return 0;
|
|
2426
2687
|
console.error("No test files found in any project");
|
|
2427
2688
|
return 2;
|
|
2428
2689
|
}
|
|
@@ -2451,12 +2712,14 @@ async function runSingleProject(config, cliFiles) {
|
|
|
2451
2712
|
resolveSetupFilePaths(config);
|
|
2452
2713
|
const discovery = discoverTestFiles(config, cliFiles);
|
|
2453
2714
|
if (discovery.files.length === 0) {
|
|
2715
|
+
if (config.passWithNoTests) return 0;
|
|
2454
2716
|
console.error("No test files found");
|
|
2455
2717
|
return 2;
|
|
2456
2718
|
}
|
|
2457
2719
|
const typeTestFiles = config.typecheck ? discovery.files.filter((file) => TYPE_TEST_PATTERN.test(file)) : [];
|
|
2458
2720
|
const runtimeTestFiles = config.typecheckOnly ? [] : discovery.files.filter((file) => !TYPE_TEST_PATTERN.test(file));
|
|
2459
2721
|
if (typeTestFiles.length === 0 && runtimeTestFiles.length === 0) {
|
|
2722
|
+
if (config.passWithNoTests) return 0;
|
|
2460
2723
|
console.error("No test files found for the selected mode");
|
|
2461
2724
|
return 2;
|
|
2462
2725
|
}
|
|
@@ -2542,16 +2805,9 @@ function validateBackend(value) {
|
|
|
2542
2805
|
function getLuauErrorHint(message) {
|
|
2543
2806
|
for (const [pattern, hint] of LUAU_ERROR_HINTS) if (pattern.test(message)) return hint;
|
|
2544
2807
|
}
|
|
2545
|
-
function normalizeFormatterName(name) {
|
|
2546
|
-
return name === "compact" ? "agent" : name;
|
|
2547
|
-
}
|
|
2548
|
-
function normalizeFormatterEntry(entry) {
|
|
2549
|
-
if (Array.isArray(entry)) return [normalizeFormatterName(entry[0]), entry[1]];
|
|
2550
|
-
return normalizeFormatterName(entry);
|
|
2551
|
-
}
|
|
2552
2808
|
function resolveFormatters(cli, config) {
|
|
2553
2809
|
const explicit = cli.formatters ?? config.formatters;
|
|
2554
|
-
if (explicit !== void 0) return explicit
|
|
2810
|
+
if (explicit !== void 0) return explicit;
|
|
2555
2811
|
const defaults = isAgent ? ["agent"] : ["default"];
|
|
2556
2812
|
if (process.env["GITHUB_ACTIONS"] === "true") defaults.push("github-actions");
|
|
2557
2813
|
return defaults;
|
|
@@ -2562,12 +2818,14 @@ function mergeCliWithConfig(cli, config) {
|
|
|
2562
2818
|
backend: cli.backend ?? config.backend,
|
|
2563
2819
|
cache: cli.cache ?? config.cache,
|
|
2564
2820
|
collectCoverage: cli.collectCoverage ?? config.collectCoverage,
|
|
2821
|
+
collectCoverageFrom: cli.collectCoverageFrom ?? config.collectCoverageFrom,
|
|
2565
2822
|
color: cli.color ?? config.color,
|
|
2566
2823
|
coverageDirectory: cli.coverageDirectory ?? config.coverageDirectory,
|
|
2567
2824
|
coverageReporters: cli.coverageReporters ?? config.coverageReporters,
|
|
2568
2825
|
formatters: resolveFormatters(cli, config),
|
|
2569
2826
|
gameOutput: cli.gameOutput ?? config.gameOutput,
|
|
2570
2827
|
outputFile: cli.outputFile ?? config.outputFile,
|
|
2828
|
+
passWithNoTests: cli.passWithNoTests ?? config.passWithNoTests,
|
|
2571
2829
|
pollInterval: cli.pollInterval ?? config.pollInterval,
|
|
2572
2830
|
port: cli.port ?? config.port,
|
|
2573
2831
|
rojoProject: cli.rojoProject ?? config.rojoProject,
|