@isentinel/jest-roblox 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/cli.mjs +6 -25
- package/dist/index.d.mts +36 -0
- package/dist/index.mjs +1 -1
- package/dist/{run-BEUPi80L.mjs → run-CyHhajiY.mjs} +516 -246
- package/dist/sea-entry.cjs +522 -266
- package/package.json +4 -4
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { PermissionError, PollTimeoutError } from "@bedrock-rbx/ocale";
|
|
2
|
+
import { PermissionError, PollTimeoutError, TRANSIENT_TRANSPORT_CODES } from "@bedrock-rbx/ocale";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { isDeepStrictEqual } from "node:util";
|
|
5
5
|
import color from "tinyrainbow";
|
|
6
6
|
import { createDefineConfig, loadConfig } from "c12";
|
|
7
7
|
import { defuFn } from "defu";
|
|
8
8
|
import * as fs$1 from "node:fs";
|
|
9
|
-
import fs, { existsSync, readFileSync
|
|
9
|
+
import fs, { existsSync, readFileSync } from "node:fs";
|
|
10
10
|
import * as path$1 from "node:path";
|
|
11
11
|
import path, { dirname, join, relative, resolve } from "node:path";
|
|
12
12
|
import { type } from "arktype";
|
|
@@ -20,6 +20,7 @@ import picomatch from "picomatch";
|
|
|
20
20
|
import { getTsconfig } from "get-tsconfig";
|
|
21
21
|
import hljs from "highlight.js/lib/core";
|
|
22
22
|
import typescript from "highlight.js/lib/languages/typescript";
|
|
23
|
+
import { performance } from "node:perf_hooks";
|
|
23
24
|
import { LuauExecutionClient } from "@bedrock-rbx/ocale/luau-execution";
|
|
24
25
|
import { PlacesClient } from "@bedrock-rbx/ocale/places";
|
|
25
26
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -29,11 +30,10 @@ import * as cp from "node:child_process";
|
|
|
29
30
|
import { execFileSync } from "node:child_process";
|
|
30
31
|
import * as os from "node:os";
|
|
31
32
|
import { Buffer } from "node:buffer";
|
|
32
|
-
import { performance } from "node:perf_hooks";
|
|
33
33
|
import { parseJSONC, parseYAML } from "confbox";
|
|
34
34
|
import { Visitor, parseSync } from "oxc-parser";
|
|
35
35
|
//#region package.json
|
|
36
|
-
var version = "0.3.
|
|
36
|
+
var version = "0.3.2";
|
|
37
37
|
//#endregion
|
|
38
38
|
//#region src/config/errors.ts
|
|
39
39
|
var ConfigError = class extends Error {
|
|
@@ -933,12 +933,35 @@ function mapBranchArmLocations(traceMap, locations, sourceMapDirectory) {
|
|
|
933
933
|
tsPath
|
|
934
934
|
};
|
|
935
935
|
}
|
|
936
|
+
/**
|
|
937
|
+
* Detects a phantom branch arm produced by a source-less synthetic statement
|
|
938
|
+
* `if` (e.g. a roblox-ts Array polyfill like `.filter`/`.includes`). The
|
|
939
|
+
* synthetic `if` has no source map entry, so trace-mapping's greatest-lower-
|
|
940
|
+
* bound bias snaps both arms onto the nearest preceding segment — the then-
|
|
941
|
+
* arm's own start — yielding a zero-width arm that coincides with another
|
|
942
|
+
* arm's start and can never be covered.
|
|
943
|
+
*
|
|
944
|
+
* A genuine statement `if` is safe: roblox-ts always renders it multi-line, so
|
|
945
|
+
* the then-body (generated line `if+1`) and the implicit-else arm (generated
|
|
946
|
+
* line `if`) carry distinct source-map segments and never collapse. This is
|
|
947
|
+
* gated to `type === "if"` by the caller: a single-line `expr-if` (ternary)
|
|
948
|
+
* legitimately collapses to one column-0 segment and must NOT be dropped.
|
|
949
|
+
*/
|
|
950
|
+
function hasCollapsedPhantomArm(locations) {
|
|
951
|
+
return locations.some((arm, index) => {
|
|
952
|
+
if (!(arm.start.line === arm.end.line && arm.start.column === arm.end.column)) return false;
|
|
953
|
+
return locations.some((other, otherIndex) => {
|
|
954
|
+
return otherIndex !== index && other.start.line === arm.start.line && other.start.column === arm.start.column;
|
|
955
|
+
});
|
|
956
|
+
});
|
|
957
|
+
}
|
|
936
958
|
function mapFileBranches(resources, fileCoverage, pendingBranches) {
|
|
937
959
|
if (resources.coverageMap.branchMap === void 0) return;
|
|
938
960
|
for (const [branchId, entry] of Object.entries(resources.coverageMap.branchMap)) {
|
|
939
961
|
const armHitCounts = fileCoverage.b?.[branchId] ?? [];
|
|
940
962
|
const result = mapBranchArmLocations(resources.traceMap, entry.locations, resources.sourceMapDirectory);
|
|
941
963
|
if (result === void 0) continue;
|
|
964
|
+
if (entry.type === "if" && hasCollapsedPhantomArm(result.locations)) continue;
|
|
942
965
|
let fileBranches = pendingBranches.get(result.tsPath);
|
|
943
966
|
if (fileBranches === void 0) {
|
|
944
967
|
fileBranches = [];
|
|
@@ -1449,6 +1472,7 @@ function convertToLuau(filePath) {
|
|
|
1449
1472
|
const RbxPathParent = Symbol("Parent");
|
|
1450
1473
|
var RojoResolver = class RojoResolver {
|
|
1451
1474
|
rbxPath = new Array();
|
|
1475
|
+
realpathCache = /* @__PURE__ */ new Map();
|
|
1452
1476
|
walkedConfigFilesInternal = /* @__PURE__ */ new Set();
|
|
1453
1477
|
walkedDirectoriesInternal = /* @__PURE__ */ new Set();
|
|
1454
1478
|
filePathToRbxPathMap = /* @__PURE__ */ new Map();
|
|
@@ -1459,12 +1483,12 @@ var RojoResolver = class RojoResolver {
|
|
|
1459
1483
|
static findRojoConfigFilePath(projectPath) {
|
|
1460
1484
|
const warnings = new Array();
|
|
1461
1485
|
const defaultPath = path.join(projectPath, ROJO_DEFAULT_NAME);
|
|
1462
|
-
if (existsSync(defaultPath)) return {
|
|
1486
|
+
if (fs$1.existsSync(defaultPath)) return {
|
|
1463
1487
|
path: defaultPath,
|
|
1464
1488
|
warnings
|
|
1465
1489
|
};
|
|
1466
1490
|
const candidates = new Array();
|
|
1467
|
-
for (const fileName of readdirSync(projectPath)) if (fileName !== ROJO_DEFAULT_NAME && (fileName === ROJO_OLD_NAME || ROJO_FILE_REGEX.test(fileName))) candidates.push(path.join(projectPath, fileName));
|
|
1491
|
+
for (const fileName of fs$1.readdirSync(projectPath)) if (fileName !== ROJO_DEFAULT_NAME && (fileName === ROJO_OLD_NAME || ROJO_FILE_REGEX.test(fileName))) candidates.push(path.join(projectPath, fileName));
|
|
1468
1492
|
if (candidates.length > 1) warnings.push(`Multiple *.project.json files found, using ${candidates[0]}`);
|
|
1469
1493
|
return {
|
|
1470
1494
|
path: candidates[0],
|
|
@@ -1600,34 +1624,42 @@ var RojoResolver = class RojoResolver {
|
|
|
1600
1624
|
get walkedDirectories() {
|
|
1601
1625
|
return this.walkedDirectoriesInternal;
|
|
1602
1626
|
}
|
|
1627
|
+
cachedRealpath(targetPath) {
|
|
1628
|
+
let resolved = this.realpathCache.get(targetPath);
|
|
1629
|
+
if (resolved === void 0) {
|
|
1630
|
+
resolved = fs$1.realpathSync(targetPath);
|
|
1631
|
+
this.realpathCache.set(targetPath, resolved);
|
|
1632
|
+
}
|
|
1633
|
+
return resolved;
|
|
1634
|
+
}
|
|
1603
1635
|
getContainer(from, rbxPath) {
|
|
1604
1636
|
if (this.isGame && rbxPath) {
|
|
1605
1637
|
for (const container of from) if (arrayStartsWith(rbxPath, container)) return container;
|
|
1606
1638
|
}
|
|
1607
1639
|
}
|
|
1608
1640
|
parseConfig(rojoConfigFilePath, doNotPush = false) {
|
|
1609
|
-
if (!existsSync(rojoConfigFilePath)) {
|
|
1641
|
+
if (!fs$1.existsSync(rojoConfigFilePath)) {
|
|
1610
1642
|
this.warn(`RojoResolver: Path does not exist "${rojoConfigFilePath}"`);
|
|
1611
1643
|
return;
|
|
1612
1644
|
}
|
|
1613
|
-
const realPath =
|
|
1645
|
+
const realPath = this.cachedRealpath(rojoConfigFilePath);
|
|
1614
1646
|
this.walkedConfigFilesInternal.add(realPath);
|
|
1615
1647
|
let configJson;
|
|
1616
1648
|
try {
|
|
1617
|
-
configJson = JSON.parse(readFileSync(realPath, "utf8"));
|
|
1649
|
+
configJson = JSON.parse(fs$1.readFileSync(realPath, "utf8"));
|
|
1618
1650
|
} catch {}
|
|
1619
1651
|
if (isValidRojoConfig(configJson)) this.parseTree(path.dirname(rojoConfigFilePath), configJson.name, configJson.tree, doNotPush);
|
|
1620
1652
|
else this.warn("RojoResolver: Invalid configuration!");
|
|
1621
1653
|
}
|
|
1622
1654
|
parsePath(itemPath) {
|
|
1623
1655
|
const luauPath = convertToLuau(itemPath);
|
|
1624
|
-
const realPath = existsSync(luauPath) ?
|
|
1656
|
+
const realPath = fs$1.existsSync(luauPath) ? this.cachedRealpath(luauPath) : luauPath;
|
|
1625
1657
|
const extension = path.extname(luauPath);
|
|
1626
1658
|
if (ROJO_MODULE_EXTS.has(extension)) this.filePathToRbxPathMap.set(luauPath, [...this.rbxPath]);
|
|
1627
1659
|
else {
|
|
1628
|
-
const isDirectory = existsSync(realPath) && statSync(realPath).isDirectory();
|
|
1660
|
+
const isDirectory = fs$1.existsSync(realPath) && fs$1.statSync(realPath).isDirectory();
|
|
1629
1661
|
if (isDirectory) this.walkedDirectoriesInternal.add(realPath);
|
|
1630
|
-
if (isDirectory && readdirSync(realPath).includes(ROJO_DEFAULT_NAME)) this.parseConfig(path.join(luauPath, ROJO_DEFAULT_NAME), true);
|
|
1662
|
+
if (isDirectory && fs$1.readdirSync(realPath).includes(ROJO_DEFAULT_NAME)) this.parseConfig(path.join(luauPath, ROJO_DEFAULT_NAME), true);
|
|
1631
1663
|
else {
|
|
1632
1664
|
this.partitions.unshift({
|
|
1633
1665
|
fsPath: luauPath,
|
|
@@ -1644,26 +1676,40 @@ var RojoResolver = class RojoResolver {
|
|
|
1644
1676
|
for (const childName of Object.keys(tree).filter((value) => !value.startsWith("$"))) this.parseTree(basePath, childName, tree[childName]);
|
|
1645
1677
|
if (!doNotPush) this.rbxPath.pop();
|
|
1646
1678
|
}
|
|
1647
|
-
searchChildren(directory,
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
if (
|
|
1679
|
+
searchChildren(directory, directoryEntries) {
|
|
1680
|
+
const projectFiles = new Array();
|
|
1681
|
+
const subDirectories = new Array();
|
|
1682
|
+
for (const entry of directoryEntries) {
|
|
1683
|
+
const childPath = path.join(directory, entry.name);
|
|
1684
|
+
let isFile = entry.isFile();
|
|
1685
|
+
let isDirectory = entry.isDirectory();
|
|
1686
|
+
if (!isFile && !isDirectory) try {
|
|
1687
|
+
const stat = fs$1.statSync(this.cachedRealpath(childPath));
|
|
1688
|
+
isFile = stat.isFile();
|
|
1689
|
+
isDirectory = stat.isDirectory();
|
|
1690
|
+
} catch (err) {
|
|
1691
|
+
this.warn(`RojoResolver: Failed to resolve "${childPath}" (${err.message})`);
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1694
|
+
if (isFile && ROJO_FILE_REGEX.test(entry.name)) projectFiles.push(childPath);
|
|
1695
|
+
else if (isDirectory) subDirectories.push({
|
|
1696
|
+
name: entry.name,
|
|
1697
|
+
path: childPath
|
|
1698
|
+
});
|
|
1655
1699
|
}
|
|
1700
|
+
for (const childPath of projectFiles) this.parseConfig(childPath);
|
|
1701
|
+
for (const { name, path: childPath } of subDirectories) this.searchDirectory(childPath, name);
|
|
1656
1702
|
}
|
|
1657
1703
|
searchDirectory(directory, item) {
|
|
1658
|
-
const realPath =
|
|
1704
|
+
const realPath = this.cachedRealpath(directory);
|
|
1659
1705
|
this.walkedDirectoriesInternal.add(realPath);
|
|
1660
|
-
const
|
|
1661
|
-
if (
|
|
1706
|
+
const directoryEntries = fs$1.readdirSync(directory, { withFileTypes: true });
|
|
1707
|
+
if (directoryEntries.some((entry) => entry.name === ROJO_DEFAULT_NAME)) {
|
|
1662
1708
|
this.parseConfig(path.join(directory, ROJO_DEFAULT_NAME));
|
|
1663
1709
|
return;
|
|
1664
1710
|
}
|
|
1665
1711
|
if (item !== void 0) this.rbxPath.push(item);
|
|
1666
|
-
this.searchChildren(directory,
|
|
1712
|
+
this.searchChildren(directory, directoryEntries);
|
|
1667
1713
|
if (item !== void 0) this.rbxPath.pop();
|
|
1668
1714
|
}
|
|
1669
1715
|
warn(str) {
|
|
@@ -2058,6 +2104,7 @@ function findMapping(filePath, mappings, key = "outDir") {
|
|
|
2058
2104
|
}
|
|
2059
2105
|
function replacePrefix(filePath, from, to) {
|
|
2060
2106
|
if (filePath === from) return to;
|
|
2107
|
+
if (from === ".") return `${to}/${filePath.startsWith("./") ? filePath.slice(2) : filePath}`;
|
|
2061
2108
|
if (filePath.startsWith(`${from}/`)) return `${to}${filePath.slice(from.length)}`;
|
|
2062
2109
|
return filePath;
|
|
2063
2110
|
}
|
|
@@ -2694,7 +2741,7 @@ function formatTestSummary(result, timing, styles, options) {
|
|
|
2694
2741
|
}
|
|
2695
2742
|
function formatResult(result, timing, options) {
|
|
2696
2743
|
const styles = createStyles(options.color, options.slowTestThreshold);
|
|
2697
|
-
const lines = [
|
|
2744
|
+
const lines = [""];
|
|
2698
2745
|
for (const file of result.testResults) {
|
|
2699
2746
|
if (options.failuresOnly === true && file.numFailingTests === 0 && !hasExecError(file)) continue;
|
|
2700
2747
|
lines.push(formatFileSummary(file, options, styles));
|
|
@@ -2868,7 +2915,7 @@ function formatMultiProjectResult(projects, timing, options) {
|
|
|
2868
2915
|
result,
|
|
2869
2916
|
styles
|
|
2870
2917
|
}));
|
|
2871
|
-
const lines = [
|
|
2918
|
+
const lines = ["", sections.join("\n\n")];
|
|
2872
2919
|
const mergedResult = mergeJestResults(projects.map((project) => project.result));
|
|
2873
2920
|
lines.push("", formatTestSummary(mergedResult, timing, styles, {
|
|
2874
2921
|
snapshotWriteFailures: options.snapshotWriteFailures,
|
|
@@ -3675,6 +3722,15 @@ function hasFormatter(formatters, name) {
|
|
|
3675
3722
|
function usesAgentFormatter(formatters, verbose = false) {
|
|
3676
3723
|
return hasFormatter(formatters, "agent") && !verbose;
|
|
3677
3724
|
}
|
|
3725
|
+
/**
|
|
3726
|
+
* Whether human-facing progress output (the run header, the "Running X of Y"
|
|
3727
|
+
* notice, the workspace streaming lines) should be written: not silent and not
|
|
3728
|
+
* a machine-readable formatter (json, or non-verbose agent). The single source
|
|
3729
|
+
* of truth so these sinks can't drift apart.
|
|
3730
|
+
*/
|
|
3731
|
+
function isDefaultHumanFormatter(options) {
|
|
3732
|
+
return options.silent !== true && !usesAgentFormatter(options.formatters, options.verbose) && !hasFormatter(options.formatters, "json");
|
|
3733
|
+
}
|
|
3678
3734
|
//#endregion
|
|
3679
3735
|
//#region src/snapshot/path-resolver.ts
|
|
3680
3736
|
function createSnapshotPathResolver(config) {
|
|
@@ -3687,7 +3743,7 @@ function createSnapshotPathResolver(config) {
|
|
|
3687
3743
|
const result = `${basePath}/${normalized.slice(prefix.length + 1)}`;
|
|
3688
3744
|
const mapping = findMapping(result, tsconfigMappings);
|
|
3689
3745
|
if (mapping !== void 0) return {
|
|
3690
|
-
filePath: replacePrefix(result, mapping.outDir, mapping.rootDir)
|
|
3746
|
+
filePath: replacePrefix(result, mapping.outDir, mapping.rootDir),
|
|
3691
3747
|
mapping
|
|
3692
3748
|
};
|
|
3693
3749
|
return { filePath: result };
|
|
@@ -3707,6 +3763,96 @@ function buildMappings(tree, prefix) {
|
|
|
3707
3763
|
return mappings;
|
|
3708
3764
|
}
|
|
3709
3765
|
//#endregion
|
|
3766
|
+
//#region src/timing/orchestration-collector.ts
|
|
3767
|
+
/**
|
|
3768
|
+
* A buffered span-tree profiler for a single, sequential host run. Nesting is
|
|
3769
|
+
* tracked with one shared stack, so spans must open and close in LIFO order:
|
|
3770
|
+
* profile a phase, and any spans it opens nest under it. It is NOT safe to run
|
|
3771
|
+
* two `profile` / `profileAsync` calls concurrently on the same collector (e.g.
|
|
3772
|
+
* `Promise.all`) — interleaved opens/closes would corrupt the stack. Create one
|
|
3773
|
+
* collector per run; `flushTimingReport` empties it so a second flush is a
|
|
3774
|
+
* no-op.
|
|
3775
|
+
*/
|
|
3776
|
+
function createTimingCollector(options = {}) {
|
|
3777
|
+
const clock = options.clock ?? { now: () => performance.now() };
|
|
3778
|
+
const sink = options.sink ?? ((line) => void process.stderr.write(`${line}\n`));
|
|
3779
|
+
const enabled = options.enabled ?? process.env["TIMING"] !== void 0;
|
|
3780
|
+
const roots = /* @__PURE__ */ new Map();
|
|
3781
|
+
const stack = [];
|
|
3782
|
+
function open(name) {
|
|
3783
|
+
const top = stack.at(-1);
|
|
3784
|
+
const node = childOf(top === void 0 ? roots : top.children, name);
|
|
3785
|
+
stack.push(node);
|
|
3786
|
+
const start = clock.now();
|
|
3787
|
+
return () => {
|
|
3788
|
+
node.elapsedMs += clock.now() - start;
|
|
3789
|
+
stack.pop();
|
|
3790
|
+
};
|
|
3791
|
+
}
|
|
3792
|
+
function profile(name, func) {
|
|
3793
|
+
if (!enabled) return func();
|
|
3794
|
+
const close = open(name);
|
|
3795
|
+
try {
|
|
3796
|
+
return func();
|
|
3797
|
+
} finally {
|
|
3798
|
+
close();
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
async function profileAsync(name, func) {
|
|
3802
|
+
if (!enabled) return func();
|
|
3803
|
+
const close = open(name);
|
|
3804
|
+
try {
|
|
3805
|
+
return await func();
|
|
3806
|
+
} finally {
|
|
3807
|
+
close();
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
function record(name, elapsedMs) {
|
|
3811
|
+
if (!enabled) return;
|
|
3812
|
+
const top = stack.at(-1);
|
|
3813
|
+
const node = childOf(top === void 0 ? roots : top.children, name);
|
|
3814
|
+
node.elapsedMs += elapsedMs;
|
|
3815
|
+
}
|
|
3816
|
+
function emit(node, depth) {
|
|
3817
|
+
sink(`[TIMING] ${" ".repeat(depth)}${node.name}: ${String(Math.round(node.elapsedMs))}ms`);
|
|
3818
|
+
for (const child of node.children.values()) emit(child, depth + 1);
|
|
3819
|
+
}
|
|
3820
|
+
function flushTimingReport() {
|
|
3821
|
+
if (!enabled || roots.size === 0) return;
|
|
3822
|
+
let total = 0;
|
|
3823
|
+
for (const node of roots.values()) {
|
|
3824
|
+
emit(node, 0);
|
|
3825
|
+
total += Math.round(node.elapsedMs);
|
|
3826
|
+
}
|
|
3827
|
+
sink(`[TIMING] TOTAL (host): ${String(total)}ms`);
|
|
3828
|
+
roots.clear();
|
|
3829
|
+
}
|
|
3830
|
+
return {
|
|
3831
|
+
flushTimingReport,
|
|
3832
|
+
profile,
|
|
3833
|
+
profileAsync,
|
|
3834
|
+
record
|
|
3835
|
+
};
|
|
3836
|
+
}
|
|
3837
|
+
/**
|
|
3838
|
+
* Shared disabled collector for callers that thread a profiler through their
|
|
3839
|
+
* signatures but are invoked outside a profiled workspace run (single-mode
|
|
3840
|
+
* coverage, the `instrument` subcommand, tests). Every method is a no-op.
|
|
3841
|
+
*/
|
|
3842
|
+
const NOOP_TIMING_COLLECTOR = createTimingCollector({ enabled: false });
|
|
3843
|
+
function childOf(parent, name) {
|
|
3844
|
+
let node = parent.get(name);
|
|
3845
|
+
if (node === void 0) {
|
|
3846
|
+
node = {
|
|
3847
|
+
name,
|
|
3848
|
+
children: /* @__PURE__ */ new Map(),
|
|
3849
|
+
elapsedMs: 0
|
|
3850
|
+
};
|
|
3851
|
+
parent.set(name, node);
|
|
3852
|
+
}
|
|
3853
|
+
return node;
|
|
3854
|
+
}
|
|
3855
|
+
//#endregion
|
|
3710
3856
|
//#region src/types/rojo.ts
|
|
3711
3857
|
const rojoProjectSchema = type({
|
|
3712
3858
|
"name": "string",
|
|
@@ -3856,38 +4002,48 @@ function formatExecuteOutput(options) {
|
|
|
3856
4002
|
* config.
|
|
3857
4003
|
*/
|
|
3858
4004
|
async function runProjects(options) {
|
|
3859
|
-
const
|
|
3860
|
-
const
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
4005
|
+
const timing = options.timing ?? NOOP_TIMING_COLLECTOR;
|
|
4006
|
+
const jobs = timing.profile("buildJobs", () => {
|
|
4007
|
+
return options.projects.map((project) => buildProjectJob(project, timing));
|
|
4008
|
+
});
|
|
4009
|
+
const { rawResults, timing: backendTiming } = await timing.profileAsync("backend.runTests", async () => {
|
|
4010
|
+
const result = await options.backend.runTests({
|
|
4011
|
+
jobs,
|
|
4012
|
+
parallel: options.parallel,
|
|
4013
|
+
scriptOverride: options.scriptOverride,
|
|
4014
|
+
streaming: options.streaming,
|
|
4015
|
+
workStealing: options.workStealing
|
|
4016
|
+
});
|
|
4017
|
+
recordBackendTimingSpans(timing, result.timing);
|
|
4018
|
+
return result;
|
|
3866
4019
|
});
|
|
3867
4020
|
if (rawResults.length !== jobs.length) throw new Error(`Backend returned ${rawResults.length.toString()} results for ${jobs.length.toString()} jobs — rawResults must be parallel to jobs`);
|
|
3868
4021
|
return {
|
|
3869
4022
|
backendTiming,
|
|
3870
|
-
results:
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
4023
|
+
results: timing.profile("processResults", () => {
|
|
4024
|
+
return rawResults.map((raw, index) => {
|
|
4025
|
+
const job = jobs[index];
|
|
4026
|
+
try {
|
|
4027
|
+
return processProjectResult(buildProjectResult(raw.entry, job, raw.fallbackGameOutput), {
|
|
4028
|
+
backendTiming,
|
|
4029
|
+
config: job.config,
|
|
4030
|
+
deferFormatting: options.deferFormatting,
|
|
4031
|
+
startTime: options.startTime,
|
|
4032
|
+
timing,
|
|
4033
|
+
version: options.version
|
|
4034
|
+
});
|
|
4035
|
+
} catch (err) {
|
|
4036
|
+
if (!(err instanceof LuauScriptError)) throw err;
|
|
4037
|
+
return buildExecutionErrorResult({
|
|
4038
|
+
backendTiming,
|
|
4039
|
+
config: job.config,
|
|
4040
|
+
deferFormatting: options.deferFormatting,
|
|
4041
|
+
error: err,
|
|
4042
|
+
startTime: options.startTime,
|
|
4043
|
+
version: options.version
|
|
4044
|
+
});
|
|
4045
|
+
}
|
|
4046
|
+
});
|
|
3891
4047
|
})
|
|
3892
4048
|
};
|
|
3893
4049
|
}
|
|
@@ -3928,6 +4084,10 @@ function parseTsconfigMappings(options) {
|
|
|
3928
4084
|
rootDir: normalizeDirectoryPath(options.rootDir ?? "src")
|
|
3929
4085
|
}];
|
|
3930
4086
|
}
|
|
4087
|
+
function recordBackendTimingSpans(timing, backendTiming) {
|
|
4088
|
+
if (backendTiming.uploadMs !== void 0) timing.record("uploadMs", backendTiming.uploadMs);
|
|
4089
|
+
timing.record("executionMs", backendTiming.executionMs);
|
|
4090
|
+
}
|
|
3931
4091
|
const EXIT_CODE_MESSAGE = /^Exited with code: \d+$/;
|
|
3932
4092
|
/**
|
|
3933
4093
|
* Compose the human-readable failure message for an exec-error file
|
|
@@ -4128,7 +4288,7 @@ function writeSnapshots(snapshotWrites, config, tsconfigMappings) {
|
|
|
4128
4288
|
fs$1.writeFileSync(absolutePath, content);
|
|
4129
4289
|
const { filePath, mapping } = resolved;
|
|
4130
4290
|
if (mapping !== void 0) {
|
|
4131
|
-
const outPath = mapping.
|
|
4291
|
+
const outPath = replacePrefix(filePath, mapping.rootDir, mapping.outDir);
|
|
4132
4292
|
const absoluteOutPath = path$1.resolve(config.rootDir, outPath);
|
|
4133
4293
|
fs$1.mkdirSync(path$1.dirname(absoluteOutPath), { recursive: true });
|
|
4134
4294
|
fs$1.writeFileSync(absoluteOutPath, content);
|
|
@@ -4157,19 +4317,23 @@ function writeSnapshots(snapshotWrites, config, tsconfigMappings) {
|
|
|
4157
4317
|
* formatter output. Called once per job.
|
|
4158
4318
|
*/
|
|
4159
4319
|
function processProjectResult(entry, options) {
|
|
4160
|
-
const { backendTiming, config, deferFormatting, startTime, version } = options;
|
|
4320
|
+
const { backendTiming, config, deferFormatting, startTime, timing, version } = options;
|
|
4161
4321
|
const { coverageData, gameOutput, luauTiming, result, setupMs, snapshotWrites } = entry;
|
|
4162
|
-
const tsconfigMappings =
|
|
4163
|
-
|
|
4322
|
+
const tsconfigMappings = timing.profile("resolveTsconfigMappings", () => {
|
|
4323
|
+
return resolveAllTsconfigMappings(config.rootDir);
|
|
4324
|
+
});
|
|
4325
|
+
const writeCounts = snapshotWrites !== void 0 ? timing.profile("writeSnapshots", () => {
|
|
4326
|
+
return writeSnapshots(snapshotWrites, config, tsconfigMappings);
|
|
4327
|
+
}) : {
|
|
4164
4328
|
attempted: 0,
|
|
4165
4329
|
failed: 0,
|
|
4166
4330
|
written: 0
|
|
4167
4331
|
};
|
|
4168
4332
|
const testsMs = calculateTestsMs(result.testResults);
|
|
4169
|
-
const sourceMapper = config.sourceMap ? buildSourceMapper(config, tsconfigMappings) : void 0;
|
|
4333
|
+
const sourceMapper = config.sourceMap ? timing.profile("buildSourceMapper", () => buildSourceMapper(config, tsconfigMappings)) : void 0;
|
|
4170
4334
|
resolveTestFilePaths(result, sourceMapper);
|
|
4171
4335
|
const totalMs = Date.now() - startTime;
|
|
4172
|
-
const
|
|
4336
|
+
const resultTiming = {
|
|
4173
4337
|
executionMs: backendTiming.executionMs,
|
|
4174
4338
|
setupMs,
|
|
4175
4339
|
startTime,
|
|
@@ -4182,7 +4346,7 @@ function processProjectResult(entry, options) {
|
|
|
4182
4346
|
result,
|
|
4183
4347
|
snapshotWriteFailures: writeCounts.failed,
|
|
4184
4348
|
sourceMapper,
|
|
4185
|
-
timing,
|
|
4349
|
+
timing: resultTiming,
|
|
4186
4350
|
version
|
|
4187
4351
|
}) : "";
|
|
4188
4352
|
if (luauTiming !== void 0) printLuauTiming(luauTiming);
|
|
@@ -4194,7 +4358,7 @@ function processProjectResult(entry, options) {
|
|
|
4194
4358
|
result,
|
|
4195
4359
|
snapshotWriteFailures: writeCounts.failed > 0 ? writeCounts.failed : void 0,
|
|
4196
4360
|
sourceMapper,
|
|
4197
|
-
timing
|
|
4361
|
+
timing: resultTiming
|
|
4198
4362
|
};
|
|
4199
4363
|
}
|
|
4200
4364
|
/**
|
|
@@ -4202,8 +4366,10 @@ function processProjectResult(entry, options) {
|
|
|
4202
4366
|
* carries its own config so the Luau runner never re-resolves or shares format
|
|
4203
4367
|
* state across projects (fixes the spike's snapshot-diff regression — C1).
|
|
4204
4368
|
*/
|
|
4205
|
-
function buildProjectJob(parameters) {
|
|
4206
|
-
const tsconfigMappings =
|
|
4369
|
+
function buildProjectJob(parameters, timing) {
|
|
4370
|
+
const tsconfigMappings = timing.profile("resolveTsconfigMappings", () => {
|
|
4371
|
+
return resolveAllTsconfigMappings(parameters.config.rootDir);
|
|
4372
|
+
});
|
|
4207
4373
|
const luauProject = isLuauProject(parameters.testFiles, tsconfigMappings);
|
|
4208
4374
|
return {
|
|
4209
4375
|
config: applySnapshotFormatDefaults(parameters.config, luauProject),
|
|
@@ -4763,7 +4929,10 @@ var OcaleRunner = class {
|
|
|
4763
4929
|
script,
|
|
4764
4930
|
timeoutSeconds,
|
|
4765
4931
|
universeId: this.credentials.universeId
|
|
4766
|
-
}, {
|
|
4932
|
+
}, {
|
|
4933
|
+
retryableTransportCodes: TRANSIENT_TRANSPORT_CODES,
|
|
4934
|
+
timeoutMs: timeout
|
|
4935
|
+
});
|
|
4767
4936
|
if (!result.success) {
|
|
4768
4937
|
if (result.err instanceof PollTimeoutError) throw new Error("Execution timed out", { cause: result.err });
|
|
4769
4938
|
throw new Error(result.err.message, { cause: result.err });
|
|
@@ -4785,7 +4954,7 @@ var OcaleRunner = class {
|
|
|
4785
4954
|
placeId: this.credentials.placeId,
|
|
4786
4955
|
universeId: this.credentials.universeId
|
|
4787
4956
|
};
|
|
4788
|
-
const result = await this.places.save(parameters);
|
|
4957
|
+
const result = await this.places.save(parameters, { retryableTransportCodes: TRANSIENT_TRANSPORT_CODES });
|
|
4789
4958
|
if (!result.success) throw new Error(`Failed to upload place: ${result.err.message}`, { cause: result.err });
|
|
4790
4959
|
return {
|
|
4791
4960
|
uploadMs: Date.now() - uploadStart,
|
|
@@ -4903,6 +5072,35 @@ function generateTestScript(options) {
|
|
|
4903
5072
|
return test_runner_bundled_default.replace("__CONFIG_JSON__", () => JSON.stringify({ configs }));
|
|
4904
5073
|
}
|
|
4905
5074
|
//#endregion
|
|
5075
|
+
//#region src/utils/error-chain.ts
|
|
5076
|
+
const MAX_DEPTH = 5;
|
|
5077
|
+
function walkErrorChain(err) {
|
|
5078
|
+
const entries = [];
|
|
5079
|
+
let current = err;
|
|
5080
|
+
while (current instanceof Error && entries.length < MAX_DEPTH) {
|
|
5081
|
+
entries.push({
|
|
5082
|
+
name: current.constructor.name,
|
|
5083
|
+
code: readStringProperty(current, "code"),
|
|
5084
|
+
errno: readStringProperty(current, "errno"),
|
|
5085
|
+
message: current.message,
|
|
5086
|
+
requiredScopes: current instanceof PermissionError ? current.requiredScopes : void 0,
|
|
5087
|
+
syscall: readStringProperty(current, "syscall")
|
|
5088
|
+
});
|
|
5089
|
+
current = current.cause;
|
|
5090
|
+
}
|
|
5091
|
+
return entries;
|
|
5092
|
+
}
|
|
5093
|
+
function formatMissingScopes(scopes) {
|
|
5094
|
+
if (scopes.length === 0) return "API key has insufficient scopes. Add via Creator Dashboard.";
|
|
5095
|
+
const joined = scopes.join(", ");
|
|
5096
|
+
return `API key missing scope${scopes.length === 1 ? "" : "s"} ${joined}. Add via Creator Dashboard.`;
|
|
5097
|
+
}
|
|
5098
|
+
function readStringProperty(err, key) {
|
|
5099
|
+
const value = Reflect.get(err, key);
|
|
5100
|
+
if (value === void 0 || value === null) return;
|
|
5101
|
+
return String(value);
|
|
5102
|
+
}
|
|
5103
|
+
//#endregion
|
|
4906
5104
|
//#region src/backends/open-cloud.ts
|
|
4907
5105
|
const PARALLEL_AUTO_CAP = 3;
|
|
4908
5106
|
const BASE_URL_ENV = "JEST_ROBLOX_OPEN_CLOUD_BASE_URL";
|
|
@@ -5038,10 +5236,7 @@ function createOpenCloudBackend(credentials) {
|
|
|
5038
5236
|
}
|
|
5039
5237
|
function describeError(err) {
|
|
5040
5238
|
const cause = err instanceof Error ? err.cause : void 0;
|
|
5041
|
-
if (cause instanceof PermissionError)
|
|
5042
|
-
const scopes = cause.requiredScopes.join(", ");
|
|
5043
|
-
return `API key missing scope${cause.requiredScopes.length === 1 ? "" : "s"} ${scopes}. Add via Creator Dashboard.`;
|
|
5044
|
-
}
|
|
5239
|
+
if (cause instanceof PermissionError) return formatMissingScopes(cause.requiredScopes);
|
|
5045
5240
|
return err instanceof Error ? err.message : String(err);
|
|
5046
5241
|
}
|
|
5047
5242
|
function warnStreamingDisabled(err, state) {
|
|
@@ -5814,6 +6009,27 @@ function narrowConfigByFiles(config, files) {
|
|
|
5814
6009
|
testPathPattern: `(${branches.join("|")})`
|
|
5815
6010
|
};
|
|
5816
6011
|
}
|
|
6012
|
+
/**
|
|
6013
|
+
* Forward an Instance-namespace `testPathPattern` to the Luau runner.
|
|
6014
|
+
*
|
|
6015
|
+
* Node-side discovery is the source of truth: the FS-namespace filter
|
|
6016
|
+
* (positional args or `--testPathPattern`) has already resolved to a concrete
|
|
6017
|
+
* file set against real paths. Drop the raw FS-shaped pattern and re-narrow by
|
|
6018
|
+
* the discovered files so Jest-on-Roblox matches the same files — its paths are
|
|
6019
|
+
* Roblox Instance names (e.g. `ServerScriptService/...`) with no `src/` prefix,
|
|
6020
|
+
* so a raw FS pattern like `src/server/foo.spec` matches zero files there.
|
|
6021
|
+
*
|
|
6022
|
+
* `filterActive` gates the rewrite: a bare run (no positionals, no
|
|
6023
|
+
* `testPathPattern`) leaves the config untouched so the Luau side runs every
|
|
6024
|
+
* `testMatch` file rather than a giant basename alternation.
|
|
6025
|
+
*/
|
|
6026
|
+
function narrowForLuauRun(config, runtimeFiles, filterActive) {
|
|
6027
|
+
if (!filterActive) return config;
|
|
6028
|
+
return narrowConfigByFiles({
|
|
6029
|
+
...config,
|
|
6030
|
+
testPathPattern: void 0
|
|
6031
|
+
}, runtimeFiles);
|
|
6032
|
+
}
|
|
5817
6033
|
function toBasenamePattern(file) {
|
|
5818
6034
|
const posix = file.replaceAll("\\", "/");
|
|
5819
6035
|
const lastSlash = posix.lastIndexOf("/");
|
|
@@ -6347,89 +6563,6 @@ function buildWithRojo(projectPath, outputPath) {
|
|
|
6347
6563
|
}
|
|
6348
6564
|
}
|
|
6349
6565
|
//#endregion
|
|
6350
|
-
//#region src/timing/orchestration-collector.ts
|
|
6351
|
-
/**
|
|
6352
|
-
* A buffered span-tree profiler for a single, sequential host run. Nesting is
|
|
6353
|
-
* tracked with one shared stack, so spans must open and close in LIFO order:
|
|
6354
|
-
* profile a phase, and any spans it opens nest under it. It is NOT safe to run
|
|
6355
|
-
* two `profile` / `profileAsync` calls concurrently on the same collector (e.g.
|
|
6356
|
-
* `Promise.all`) — interleaved opens/closes would corrupt the stack. Create one
|
|
6357
|
-
* collector per run; `flushTimingReport` empties it so a second flush is a
|
|
6358
|
-
* no-op.
|
|
6359
|
-
*/
|
|
6360
|
-
function createTimingCollector(options = {}) {
|
|
6361
|
-
const clock = options.clock ?? { now: () => performance.now() };
|
|
6362
|
-
const sink = options.sink ?? ((line) => void process.stderr.write(`${line}\n`));
|
|
6363
|
-
const enabled = options.enabled ?? process.env["TIMING"] !== void 0;
|
|
6364
|
-
const roots = /* @__PURE__ */ new Map();
|
|
6365
|
-
const stack = [];
|
|
6366
|
-
function open(name) {
|
|
6367
|
-
const top = stack.at(-1);
|
|
6368
|
-
const node = childOf(top === void 0 ? roots : top.children, name);
|
|
6369
|
-
stack.push(node);
|
|
6370
|
-
const start = clock.now();
|
|
6371
|
-
return () => {
|
|
6372
|
-
node.elapsedMs += clock.now() - start;
|
|
6373
|
-
stack.pop();
|
|
6374
|
-
};
|
|
6375
|
-
}
|
|
6376
|
-
function profile(name, func) {
|
|
6377
|
-
if (!enabled) return func();
|
|
6378
|
-
const close = open(name);
|
|
6379
|
-
try {
|
|
6380
|
-
return func();
|
|
6381
|
-
} finally {
|
|
6382
|
-
close();
|
|
6383
|
-
}
|
|
6384
|
-
}
|
|
6385
|
-
async function profileAsync(name, func) {
|
|
6386
|
-
if (!enabled) return func();
|
|
6387
|
-
const close = open(name);
|
|
6388
|
-
try {
|
|
6389
|
-
return await func();
|
|
6390
|
-
} finally {
|
|
6391
|
-
close();
|
|
6392
|
-
}
|
|
6393
|
-
}
|
|
6394
|
-
function emit(node, depth) {
|
|
6395
|
-
sink(`[TIMING] ${" ".repeat(depth)}${node.name}: ${String(Math.round(node.elapsedMs))}ms`);
|
|
6396
|
-
for (const child of node.children.values()) emit(child, depth + 1);
|
|
6397
|
-
}
|
|
6398
|
-
function flushTimingReport() {
|
|
6399
|
-
if (!enabled || roots.size === 0) return;
|
|
6400
|
-
let total = 0;
|
|
6401
|
-
for (const node of roots.values()) {
|
|
6402
|
-
emit(node, 0);
|
|
6403
|
-
total += Math.round(node.elapsedMs);
|
|
6404
|
-
}
|
|
6405
|
-
sink(`[TIMING] TOTAL (host): ${String(total)}ms`);
|
|
6406
|
-
roots.clear();
|
|
6407
|
-
}
|
|
6408
|
-
return {
|
|
6409
|
-
flushTimingReport,
|
|
6410
|
-
profile,
|
|
6411
|
-
profileAsync
|
|
6412
|
-
};
|
|
6413
|
-
}
|
|
6414
|
-
/**
|
|
6415
|
-
* Shared disabled collector for callers that thread a profiler through their
|
|
6416
|
-
* signatures but are invoked outside a profiled workspace run (single-mode
|
|
6417
|
-
* coverage, the `instrument` subcommand, tests). Every method is a no-op.
|
|
6418
|
-
*/
|
|
6419
|
-
const NOOP_TIMING_COLLECTOR = createTimingCollector({ enabled: false });
|
|
6420
|
-
function childOf(parent, name) {
|
|
6421
|
-
let node = parent.get(name);
|
|
6422
|
-
if (node === void 0) {
|
|
6423
|
-
node = {
|
|
6424
|
-
name,
|
|
6425
|
-
children: /* @__PURE__ */ new Map(),
|
|
6426
|
-
elapsedMs: 0
|
|
6427
|
-
};
|
|
6428
|
-
parent.set(name, node);
|
|
6429
|
-
}
|
|
6430
|
-
return node;
|
|
6431
|
-
}
|
|
6432
|
-
//#endregion
|
|
6433
6566
|
//#region src/utils/hash.ts
|
|
6434
6567
|
function hashBuffer(data) {
|
|
6435
6568
|
return createHash("sha256").update(data).digest("hex");
|
|
@@ -7900,15 +8033,47 @@ function classifyTestFiles(files, config) {
|
|
|
7900
8033
|
typeTestFiles
|
|
7901
8034
|
};
|
|
7902
8035
|
}
|
|
8036
|
+
function resolveAllSetupFilePaths(configs) {
|
|
8037
|
+
const resolvers = /* @__PURE__ */ new Map();
|
|
8038
|
+
for (const config of configs) {
|
|
8039
|
+
if (config.setupFiles === void 0 && config.setupFilesAfterEnv === void 0) continue;
|
|
8040
|
+
const rojoConfigPath = path$1.resolve(config.rootDir, config.rojoProject ?? DEFAULT_ROJO_PROJECT$1);
|
|
8041
|
+
const key = JSON.stringify([config.rootDir, rojoConfigPath]);
|
|
8042
|
+
let resolve = resolvers.get(key);
|
|
8043
|
+
if (resolve === void 0) {
|
|
8044
|
+
resolve = createSetupResolver({
|
|
8045
|
+
configDirectory: config.rootDir,
|
|
8046
|
+
rojoConfigPath
|
|
8047
|
+
});
|
|
8048
|
+
resolvers.set(key, resolve);
|
|
8049
|
+
}
|
|
8050
|
+
if (config.setupFiles !== void 0) config.setupFiles = config.setupFiles.map(resolve);
|
|
8051
|
+
if (config.setupFilesAfterEnv !== void 0) config.setupFilesAfterEnv = config.setupFilesAfterEnv.map(resolve);
|
|
8052
|
+
}
|
|
8053
|
+
}
|
|
7903
8054
|
function resolveSetupFilePaths(config) {
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
8055
|
+
resolveAllSetupFilePaths([config]);
|
|
8056
|
+
}
|
|
8057
|
+
//#endregion
|
|
8058
|
+
//#region src/run/run-header.ts
|
|
8059
|
+
/**
|
|
8060
|
+
* Print the ` RUN vX.Y <rootDir>` header to stdout at the moment a run begins
|
|
8061
|
+
* (right before the backend uploads), so the CLI doesn't look stalled while it
|
|
8062
|
+
* waits for remote results. The end-of-run formatters no longer emit it.
|
|
8063
|
+
*
|
|
8064
|
+
* Self-gates to the default human formatter: nothing is written under
|
|
8065
|
+
* `--silent`, `--formatters json`, or `--formatters agent` (without
|
|
8066
|
+
* `--verbose`), which produce machine-readable output that must stay clean.
|
|
8067
|
+
*/
|
|
8068
|
+
function emitRunHeader(input) {
|
|
8069
|
+
if (!isDefaultHumanFormatter(input)) return;
|
|
8070
|
+
process.stdout.write(formatRunHeader({
|
|
8071
|
+
collectCoverage: input.collectCoverage,
|
|
8072
|
+
color: input.color,
|
|
8073
|
+
rootDir: input.rootDir,
|
|
8074
|
+
verbose: input.verbose ?? false,
|
|
8075
|
+
version: input.version
|
|
8076
|
+
}));
|
|
7912
8077
|
}
|
|
7913
8078
|
//#endregion
|
|
7914
8079
|
//#region src/run/multi.ts
|
|
@@ -7916,30 +8081,61 @@ const DEFAULT_ROJO_PROJECT = "default.project.json";
|
|
|
7916
8081
|
const VERSION$2 = version;
|
|
7917
8082
|
async function runMultiProject(options) {
|
|
7918
8083
|
const { cli, config: rootConfig, rawProjects } = options;
|
|
7919
|
-
const
|
|
7920
|
-
|
|
7921
|
-
const
|
|
8084
|
+
const timing = options.timing ?? NOOP_TIMING_COLLECTOR;
|
|
8085
|
+
const rojoTree = timing.profile("loadRojoTree", () => loadRojoTree(rootConfig));
|
|
8086
|
+
const allProjects = await timing.profileAsync("resolveAllProjects", async () => {
|
|
8087
|
+
return resolveAllProjects(rawProjects, rootConfig, rojoTree, rootConfig.rootDir);
|
|
8088
|
+
});
|
|
8089
|
+
timing.profile("resolveSetupFilePaths", () => {
|
|
8090
|
+
resolveAllSetupFilePaths(allProjects.map((project) => project.config));
|
|
8091
|
+
});
|
|
8092
|
+
const { filesByProject, projects } = timing.profile("selectProjects", () => {
|
|
8093
|
+
return selectProjects(allProjects, cli.project, cli.files, rootConfig.rootDir);
|
|
8094
|
+
});
|
|
7922
8095
|
const cacheRoot = path$1.resolve(rootConfig.rootDir, ".jest-roblox", "cache");
|
|
7923
|
-
const cleaned = cleanLeftoverStubs
|
|
8096
|
+
const cleaned = timing.profile("cleanLeftoverStubs", () => {
|
|
8097
|
+
return cleanLeftoverStubs(projects, rootConfig.rootDir);
|
|
8098
|
+
});
|
|
7924
8099
|
if (cleaned.length > 0) process.stderr.write(`jest-roblox: cleaned ${String(cleaned.length)} leftover stub(s):\n${cleaned.map((stubPath) => ` ${stubPath}\n`).join("")}`);
|
|
7925
|
-
generateProjectStubs
|
|
7926
|
-
|
|
7927
|
-
|
|
8100
|
+
timing.profile("generateProjectStubs", () => {
|
|
8101
|
+
generateProjectStubs(projects, rootConfig.rootDir, cacheRoot);
|
|
8102
|
+
});
|
|
8103
|
+
const { effectiveConfig, preCoverageMs } = timing.profile("prepareCoverage", () => {
|
|
8104
|
+
return prepareMultiProjectCoverage(rootConfig, projects, cacheRoot);
|
|
8105
|
+
});
|
|
8106
|
+
const backend = await timing.profileAsync("resolveBackend", async () => {
|
|
8107
|
+
return resolveBackend(cli, effectiveConfig);
|
|
8108
|
+
});
|
|
7928
8109
|
const parallel = effectiveParallelForBackend(effectiveConfig.parallel, backend);
|
|
7929
|
-
if (!rootConfig.collectCoverage && backend.kind === "open-cloud") buildOpenCloudPlace
|
|
7930
|
-
|
|
7931
|
-
cliFiles: cli.files,
|
|
7932
|
-
effectivePlaceFile: effectiveConfig.placeFile,
|
|
7933
|
-
filesByProject,
|
|
7934
|
-
projects,
|
|
7935
|
-
rootConfig
|
|
8110
|
+
if (!rootConfig.collectCoverage && backend.kind === "open-cloud") timing.profile("buildOpenCloudPlace", () => {
|
|
8111
|
+
buildOpenCloudPlace(rootConfig, projects, cacheRoot);
|
|
7936
8112
|
});
|
|
7937
|
-
const
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
|
|
8113
|
+
const { allTypeTestFiles, pendingJobs } = timing.profile("collectPendingJobs", () => {
|
|
8114
|
+
return collectPendingJobs({
|
|
8115
|
+
cliFiles: cli.files,
|
|
8116
|
+
effectivePlaceFile: effectiveConfig.placeFile,
|
|
8117
|
+
filesByProject,
|
|
8118
|
+
projects,
|
|
8119
|
+
rootConfig
|
|
8120
|
+
});
|
|
8121
|
+
});
|
|
8122
|
+
if (pendingJobs.length > 0) emitRunHeader({
|
|
8123
|
+
collectCoverage: rootConfig.collectCoverage,
|
|
8124
|
+
color: rootConfig.color,
|
|
8125
|
+
formatters: rootConfig.formatters,
|
|
7941
8126
|
rootDir: rootConfig.rootDir,
|
|
7942
|
-
|
|
8127
|
+
silent: rootConfig.silent,
|
|
8128
|
+
verbose: rootConfig.verbose,
|
|
8129
|
+
version: VERSION$2
|
|
8130
|
+
});
|
|
8131
|
+
const projectResults = await runJobs(backend, pendingJobs, parallel, timing);
|
|
8132
|
+
const uniqueTypeTestFiles = [...new Set(allTypeTestFiles)];
|
|
8133
|
+
const typecheckResult = uniqueTypeTestFiles.length > 0 ? timing.profile("runTypecheck", () => {
|
|
8134
|
+
return runTypecheck({
|
|
8135
|
+
files: uniqueTypeTestFiles,
|
|
8136
|
+
rootDir: rootConfig.rootDir,
|
|
8137
|
+
tsconfig: rootConfig.typecheckTsconfig
|
|
8138
|
+
});
|
|
7943
8139
|
}) : void 0;
|
|
7944
8140
|
if (projectResults.length === 0 && typecheckResult === void 0) {
|
|
7945
8141
|
if (rootConfig.passWithNoTests) return {
|
|
@@ -8003,10 +8199,11 @@ function collectPendingJobs(arguments_) {
|
|
|
8003
8199
|
testMatch: project.include
|
|
8004
8200
|
};
|
|
8005
8201
|
const { runtimeFiles, typeTestFiles } = classifyTestFiles(discoverTestFiles(discoveryConfig, projectCliFiles).files, rootConfig);
|
|
8006
|
-
const
|
|
8202
|
+
const filterActive = (projectCliFiles?.length ?? 0) > 0 || discoveryConfig.testPathPattern !== void 0;
|
|
8203
|
+
const projConfig = narrowForLuauRun({
|
|
8007
8204
|
...discoveryConfig,
|
|
8008
8205
|
testMatch: project.testMatch
|
|
8009
|
-
},
|
|
8206
|
+
}, runtimeFiles, filterActive);
|
|
8010
8207
|
allTypeTestFiles.push(...typeTestFiles);
|
|
8011
8208
|
if (runtimeFiles.length === 0) continue;
|
|
8012
8209
|
const runtimeInjectionPaths = [];
|
|
@@ -8027,28 +8224,31 @@ function collectPendingJobs(arguments_) {
|
|
|
8027
8224
|
pendingJobs
|
|
8028
8225
|
};
|
|
8029
8226
|
}
|
|
8030
|
-
async function runJobs(backend, pendingJobs, parallel) {
|
|
8227
|
+
async function runJobs(backend, pendingJobs, parallel, timing) {
|
|
8031
8228
|
if (pendingJobs.length === 0) {
|
|
8032
8229
|
await backend.close?.();
|
|
8033
8230
|
return [];
|
|
8034
8231
|
}
|
|
8035
8232
|
let runResult;
|
|
8036
8233
|
try {
|
|
8037
|
-
runResult = await runProjects({
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8234
|
+
runResult = await timing.profileAsync("runProjects", async () => {
|
|
8235
|
+
return runProjects({
|
|
8236
|
+
backend,
|
|
8237
|
+
deferFormatting: true,
|
|
8238
|
+
parallel,
|
|
8239
|
+
projects: pendingJobs.map((pending) => {
|
|
8240
|
+
return {
|
|
8241
|
+
config: pending.config,
|
|
8242
|
+
displayColor: pending.displayColor,
|
|
8243
|
+
displayName: pending.displayName,
|
|
8244
|
+
runtimeInjectionPaths: pending.runtimeInjectionPaths,
|
|
8245
|
+
testFiles: pending.runtimeFiles
|
|
8246
|
+
};
|
|
8247
|
+
}),
|
|
8248
|
+
startTime: Date.now(),
|
|
8249
|
+
timing,
|
|
8250
|
+
version: VERSION$2
|
|
8251
|
+
});
|
|
8052
8252
|
});
|
|
8053
8253
|
} finally {
|
|
8054
8254
|
await backend.close?.();
|
|
@@ -8125,11 +8325,16 @@ function selectProjects(allProjects, projectNames, cliFiles, rootDirectory) {
|
|
|
8125
8325
|
const VERSION$1 = version;
|
|
8126
8326
|
async function runSingleProject(options) {
|
|
8127
8327
|
const { cli } = options;
|
|
8128
|
-
const
|
|
8129
|
-
|
|
8130
|
-
|
|
8328
|
+
const timing = options.timing ?? NOOP_TIMING_COLLECTOR;
|
|
8329
|
+
const baseConfig = { ...options.config };
|
|
8330
|
+
timing.profile("resolveSetupFilePaths", () => {
|
|
8331
|
+
resolveSetupFilePaths(baseConfig);
|
|
8332
|
+
});
|
|
8333
|
+
const discovery = timing.profile("discoverTestFiles", () => {
|
|
8334
|
+
return discoverTestFiles(baseConfig, cli.files);
|
|
8335
|
+
});
|
|
8131
8336
|
if (discovery.files.length === 0) {
|
|
8132
|
-
if (
|
|
8337
|
+
if (baseConfig.passWithNoTests) return {
|
|
8133
8338
|
mode: "single",
|
|
8134
8339
|
preCoverageMs: 0
|
|
8135
8340
|
};
|
|
@@ -8140,7 +8345,13 @@ async function runSingleProject(options) {
|
|
|
8140
8345
|
validationExitCode: 2
|
|
8141
8346
|
};
|
|
8142
8347
|
}
|
|
8143
|
-
const { runtimeFiles, typeTestFiles } = classifyTestFiles
|
|
8348
|
+
const { runtimeFiles, typeTestFiles } = timing.profile("classifyTestFiles", () => {
|
|
8349
|
+
return classifyTestFiles(discovery.files, baseConfig);
|
|
8350
|
+
});
|
|
8351
|
+
const filterActive = (cli.files?.length ?? 0) > 0 || baseConfig.testPathPattern !== void 0;
|
|
8352
|
+
const config = timing.profile("narrowForLuauRun", () => {
|
|
8353
|
+
return narrowForLuauRun(baseConfig, runtimeFiles, filterActive);
|
|
8354
|
+
});
|
|
8144
8355
|
if (typeTestFiles.length === 0 && runtimeFiles.length === 0) {
|
|
8145
8356
|
if (config.passWithNoTests) return {
|
|
8146
8357
|
mode: "single",
|
|
@@ -8157,19 +8368,27 @@ async function runSingleProject(options) {
|
|
|
8157
8368
|
let effectiveConfig = config;
|
|
8158
8369
|
if (config.collectCoverage && !config.typecheckOnly && runtimeFiles.length > 0) {
|
|
8159
8370
|
const preCoverageStart = Date.now();
|
|
8160
|
-
const { placeFile } = prepareCoverage(config);
|
|
8371
|
+
const { placeFile } = timing.profile("prepareCoverage", () => prepareCoverage(config));
|
|
8161
8372
|
preCoverageMs = Date.now() - preCoverageStart;
|
|
8162
8373
|
effectiveConfig = {
|
|
8163
8374
|
...config,
|
|
8164
8375
|
placeFile
|
|
8165
8376
|
};
|
|
8166
8377
|
}
|
|
8167
|
-
const typecheckResult = typeTestFiles.length > 0 ? runTypecheck({
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
|
|
8378
|
+
const typecheckResult = typeTestFiles.length > 0 ? timing.profile("runTypecheck", () => {
|
|
8379
|
+
return runTypecheck({
|
|
8380
|
+
files: typeTestFiles,
|
|
8381
|
+
rootDir: effectiveConfig.rootDir,
|
|
8382
|
+
tsconfig: effectiveConfig.typecheckTsconfig
|
|
8383
|
+
});
|
|
8384
|
+
}) : void 0;
|
|
8385
|
+
const runtimeResult = runtimeFiles.length > 0 ? await executeRuntimeTests({
|
|
8386
|
+
cli,
|
|
8387
|
+
config: effectiveConfig,
|
|
8388
|
+
testFiles: runtimeFiles,
|
|
8389
|
+
timing,
|
|
8390
|
+
totalFiles: discovery.totalFiles
|
|
8171
8391
|
}) : void 0;
|
|
8172
|
-
const runtimeResult = runtimeFiles.length > 0 ? await executeRuntimeTests(options, effectiveConfig, runtimeFiles, discovery.totalFiles) : void 0;
|
|
8173
8392
|
return {
|
|
8174
8393
|
mode: "single",
|
|
8175
8394
|
preCoverageMs,
|
|
@@ -8177,19 +8396,35 @@ async function runSingleProject(options) {
|
|
|
8177
8396
|
typecheckResult
|
|
8178
8397
|
};
|
|
8179
8398
|
}
|
|
8180
|
-
async function executeRuntimeTests(options
|
|
8181
|
-
|
|
8182
|
-
const
|
|
8399
|
+
async function executeRuntimeTests(options) {
|
|
8400
|
+
const { cli, config, testFiles, timing, totalFiles } = options;
|
|
8401
|
+
const useDefaultFormatter = isDefaultHumanFormatter(config);
|
|
8402
|
+
emitRunHeader({
|
|
8403
|
+
collectCoverage: config.collectCoverage,
|
|
8404
|
+
color: config.color,
|
|
8405
|
+
formatters: config.formatters,
|
|
8406
|
+
rootDir: config.rootDir,
|
|
8407
|
+
silent: config.silent,
|
|
8408
|
+
verbose: config.verbose,
|
|
8409
|
+
version: VERSION$1
|
|
8410
|
+
});
|
|
8411
|
+
if (useDefaultFormatter && testFiles.length !== totalFiles) process.stderr.write(`Running ${String(testFiles.length)} of ${String(totalFiles)} test files\n`);
|
|
8412
|
+
const backend = await timing.profileAsync("resolveBackend", async () => {
|
|
8413
|
+
return resolveBackend(cli, config);
|
|
8414
|
+
});
|
|
8183
8415
|
try {
|
|
8184
|
-
const { results } = await runProjects({
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8416
|
+
const { results } = await timing.profileAsync("runProjects", async () => {
|
|
8417
|
+
return runProjects({
|
|
8418
|
+
backend,
|
|
8419
|
+
deferFormatting: true,
|
|
8420
|
+
projects: [{
|
|
8421
|
+
config,
|
|
8422
|
+
testFiles
|
|
8423
|
+
}],
|
|
8424
|
+
startTime: Date.now(),
|
|
8425
|
+
timing,
|
|
8426
|
+
version: VERSION$1
|
|
8427
|
+
});
|
|
8193
8428
|
});
|
|
8194
8429
|
return results[0];
|
|
8195
8430
|
} finally {
|
|
@@ -8925,12 +9160,7 @@ const SYNTHESIZED_PLACE_FILE = "synthesized.rbxl";
|
|
|
8925
9160
|
const WORKSPACE_CACHE_DIRECTORY = path$1.join(".jest-roblox", "workspace");
|
|
8926
9161
|
const ROJO_PROJECT_DEFAULT = "test.project.json";
|
|
8927
9162
|
async function runWorkspace(options) {
|
|
8928
|
-
|
|
8929
|
-
try {
|
|
8930
|
-
return await runWorkspaceProfiled(options, timing);
|
|
8931
|
-
} finally {
|
|
8932
|
-
timing.flushTimingReport();
|
|
8933
|
-
}
|
|
9163
|
+
return runWorkspaceProfiled(options, options.timing ?? NOOP_TIMING_COLLECTOR);
|
|
8934
9164
|
}
|
|
8935
9165
|
function buildCoverageMap(entries) {
|
|
8936
9166
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -9044,6 +9274,7 @@ async function runWorkspaceProfiled(options, timing) {
|
|
|
9044
9274
|
};
|
|
9045
9275
|
}),
|
|
9046
9276
|
startTime,
|
|
9277
|
+
timing,
|
|
9047
9278
|
version,
|
|
9048
9279
|
...dispatchSpec
|
|
9049
9280
|
});
|
|
@@ -9110,13 +9341,33 @@ async function prepareWorkspaceDispatch(input) {
|
|
|
9110
9341
|
}
|
|
9111
9342
|
return { scriptOverride: generateMaterializerScript(inputs) };
|
|
9112
9343
|
}
|
|
9344
|
+
/**
|
|
9345
|
+
* Resolve a `--testPathPattern` against this package's files Node-side, then
|
|
9346
|
+
* forward an Instance-namespace basename pattern (see {@link narrowForLuauRun}).
|
|
9347
|
+
*
|
|
9348
|
+
* A pattern that matches no file in this package simply targets a different
|
|
9349
|
+
* package: keep the (zero-matching) raw pattern so Jest-on-Roblox runs nothing,
|
|
9350
|
+
* and set `passWithNoTests` so it doesn't `exit(1)`. The raw pattern is
|
|
9351
|
+
* load-bearing here — clearing it would drop the filter entirely and make the
|
|
9352
|
+
* Luau side fall back to `testMatch`, running the whole package.
|
|
9353
|
+
*/
|
|
9354
|
+
function narrowPackageTestPathPattern(packageConfig) {
|
|
9355
|
+
if (packageConfig.testPathPattern === void 0) return packageConfig;
|
|
9356
|
+
const { files } = discoverTestFiles(packageConfig);
|
|
9357
|
+
const { runtimeFiles } = classifyTestFiles(files, packageConfig);
|
|
9358
|
+
if (runtimeFiles.length === 0) return {
|
|
9359
|
+
...packageConfig,
|
|
9360
|
+
passWithNoTests: true
|
|
9361
|
+
};
|
|
9362
|
+
return narrowForLuauRun(packageConfig, runtimeFiles, true);
|
|
9363
|
+
}
|
|
9113
9364
|
async function loadPackages(input) {
|
|
9114
9365
|
const { cli, packageInfos, timing } = input;
|
|
9115
9366
|
const loaded = [];
|
|
9116
9367
|
for (const info of packageInfos) {
|
|
9117
|
-
const packageConfig = mergeCliWithConfig(cli, await timing.profileAsync(`load-config:${info.name}`, async () => {
|
|
9368
|
+
const packageConfig = narrowPackageTestPathPattern(mergeCliWithConfig(cli, await timing.profileAsync(`load-config:${info.name}`, async () => {
|
|
9118
9369
|
return loadConfig$1(void 0, info.packageDirectory);
|
|
9119
|
-
}));
|
|
9370
|
+
})));
|
|
9120
9371
|
const rojoProject = packageConfig.rojoProject ?? ROJO_PROJECT_DEFAULT;
|
|
9121
9372
|
const hasExplicitIgnore = packageConfig.coveragePathIgnorePatterns !== DEFAULT_CONFIG.coveragePathIgnorePatterns;
|
|
9122
9373
|
const hasExplicitCoverageCache = packageConfig.coverageCache !== DEFAULT_CONFIG.coverageCache;
|
|
@@ -9642,7 +9893,7 @@ const EMPTY_RESULT = {
|
|
|
9642
9893
|
preCoverageMs: 0,
|
|
9643
9894
|
projectResults: []
|
|
9644
9895
|
};
|
|
9645
|
-
async function runWorkspaceMode(cli, workspace) {
|
|
9896
|
+
async function runWorkspaceMode(cli, workspace, timing) {
|
|
9646
9897
|
const basicValidation = validateBasicWorkspaceFlags(cli);
|
|
9647
9898
|
if (!basicValidation.ok) return {
|
|
9648
9899
|
...EMPTY_RESULT,
|
|
@@ -9708,6 +9959,14 @@ async function runWorkspaceMode(cli, workspace) {
|
|
|
9708
9959
|
}
|
|
9709
9960
|
let runtimeResults;
|
|
9710
9961
|
try {
|
|
9962
|
+
emitRunHeader({
|
|
9963
|
+
color: runOptions.color,
|
|
9964
|
+
formatters: runOptions.formatters,
|
|
9965
|
+
rootDir: workspaceRoot,
|
|
9966
|
+
silent: runOptions.silent,
|
|
9967
|
+
verbose: cli.verbose,
|
|
9968
|
+
version: VERSION
|
|
9969
|
+
});
|
|
9711
9970
|
const onStreamingResult = resolveStreamingProgressSink(runOptions, cli);
|
|
9712
9971
|
runtimeResults = await runWorkspace({
|
|
9713
9972
|
backend,
|
|
@@ -9715,6 +9974,7 @@ async function runWorkspaceMode(cli, workspace) {
|
|
|
9715
9974
|
...onStreamingResult !== void 0 ? { onStreamingResult } : {},
|
|
9716
9975
|
packageInfos,
|
|
9717
9976
|
runOptions,
|
|
9977
|
+
timing,
|
|
9718
9978
|
version: VERSION,
|
|
9719
9979
|
workspaceRoot,
|
|
9720
9980
|
workStealingCredentials
|
|
@@ -9804,8 +10064,11 @@ function composeWorkspaceDisplayName(package_, project) {
|
|
|
9804
10064
|
* either break the structured output or be silenced anyway.
|
|
9805
10065
|
*/
|
|
9806
10066
|
function resolveStreamingProgressSink(runOptions, cli) {
|
|
9807
|
-
if (
|
|
9808
|
-
|
|
10067
|
+
if (!isDefaultHumanFormatter({
|
|
10068
|
+
formatters: runOptions.formatters,
|
|
10069
|
+
silent: runOptions.silent,
|
|
10070
|
+
verbose: cli.verbose
|
|
10071
|
+
})) return;
|
|
9809
10072
|
return (entry) => {
|
|
9810
10073
|
const line = formatStreamingProgressLine(entry, { color: runOptions.color });
|
|
9811
10074
|
process.stdout.write(`${line}\n`);
|
|
@@ -9817,18 +10080,25 @@ function isWorkspaceInvocation(cli) {
|
|
|
9817
10080
|
return cli.workspace === true || cli.packages !== void 0 || cli.affectedSince !== void 0;
|
|
9818
10081
|
}
|
|
9819
10082
|
async function runJestRoblox(cli, config) {
|
|
9820
|
-
|
|
9821
|
-
|
|
9822
|
-
|
|
9823
|
-
|
|
9824
|
-
|
|
9825
|
-
|
|
9826
|
-
|
|
9827
|
-
|
|
9828
|
-
|
|
9829
|
-
|
|
9830
|
-
|
|
9831
|
-
|
|
10083
|
+
const timing = createTimingCollector();
|
|
10084
|
+
try {
|
|
10085
|
+
if (isWorkspaceInvocation(cli)) return await runWorkspaceMode(cli, config.workspace, timing);
|
|
10086
|
+
const merged = mergeCliWithConfig(cli, config);
|
|
10087
|
+
const rawProjects = merged.projects;
|
|
10088
|
+
if (rawProjects !== void 0 && rawProjects.length > 0) return await runMultiProject({
|
|
10089
|
+
cli,
|
|
10090
|
+
config: merged,
|
|
10091
|
+
rawProjects,
|
|
10092
|
+
timing
|
|
10093
|
+
});
|
|
10094
|
+
return await runSingleProject({
|
|
10095
|
+
cli,
|
|
10096
|
+
config: merged,
|
|
10097
|
+
timing
|
|
10098
|
+
});
|
|
10099
|
+
} finally {
|
|
10100
|
+
timing.flushTimingReport();
|
|
10101
|
+
}
|
|
9832
10102
|
}
|
|
9833
10103
|
//#endregion
|
|
9834
|
-
export {
|
|
10104
|
+
export { extractJsonFromOutput as A, VALID_BACKENDS as B, formatJson as C, formatTestSummary as D, formatResult as E, DEFAULT_CONFIG as F, version as G, defineProject as H, GLOBAL_TEST_KEYS as I, JEST_ARGV_EXCLUDED_KEYS as L, mergeCliWithConfig as M, loadConfig$1 as N, formatBanner as O, resolveConfig as P, ROOT_CLI_KEYS as R, writeGameOutput as S, formatFailure as T, isValidBackend as U, defineConfig as V, ConfigError as W, formatJobSummary as _, visitStatement as a, formatGameOutputNotice as b, OpenCloudBackend as c, walkErrorChain as d, buildJestArgv as f, formatAnnotations as g, outputSingleResult as h, visitExpression as i, parseJestOutput as j, LuauScriptError as k, createOpenCloudBackend as l, outputMultiResult as m, runTypecheck as n, StudioBackend as o, generateTestScript as p, visitBlock as r, createStudioBackend as s, runJestRoblox as t, formatMissingScopes as u, formatExecuteOutput as v, writeJsonFile$1 as w, parseGameOutput as x, runProjects as y, SHARED_TEST_KEYS as z };
|