gdharness 1.3.21 → 1.3.23
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/build/cli.js +370 -296
- package/build/godot/addons/gdharness_editor/file_reads.gd +11 -4
- package/build/godot/addons/gdharness_editor/serialisation.gd +4 -6
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +46 -27
- package/build/godot/addons/gdharness_editor/value_shapes.gd +32 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +4 -6
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +32 -0
- package/build/godot/operations/file_reads.gd +11 -4
- package/build/godot/operations/serialisation.gd +4 -6
- package/build/godot/operations/value_shapes.gd +32 -0
- package/build/godot/tests/listed.gd +96 -4
- package/build/keeper.js +7 -1
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -10201,7 +10201,7 @@ var EXIT_LEAK, WALK_BEGAN = "gdharness: the engine's shutdown began clearing scr
|
|
|
10201
10201
|
var init_engine_leaks = __esm(() => {
|
|
10202
10202
|
init_class_cache();
|
|
10203
10203
|
init_gdscript_source();
|
|
10204
|
-
EXIT_LEAK = /ObjectDB instances? (?:was |were )?leaked at exit|resources? still in use at exit|Pages in use exist at exit/;
|
|
10204
|
+
EXIT_LEAK = /ObjectDB instances? (?:was |were )?leaked at exit|resources? still in use at exit|Pages in use exist at exit|RIDs? of type ["'][^"']+["'] (?:was|were) leaked/;
|
|
10205
10205
|
WALK_STOPPED_SENTENCE = "The engine's shutdown stopped part way through its scripts (godot#117991, fixed in 4.8 by " + "godot#120976), so it reported as leaked at exit what it never got to free, and the report is the " + "engine's rather than the run's. gdharness holds every script loaded from a file past that walk, " + "which keeps it whole, so this one stopped on a script it could not hold, such as one made in code. " + "A leak of the run's own is not told apart in such a run.";
|
|
10206
10206
|
EXPORTED = new RegExp(`@export(?!_(?:group|subgroup|category)\\b)\\w*(?:\\([^)]*\\))?\\s+(?:@${NAME2}(?:\\([^)]*\\))?\\s+)*` + `(?:static\\s+)?var\\s+(${NAME2})\\s*:\\s*(?:Array|Dictionary)\\s*\\[([^\\]]*)\\]`, "gu");
|
|
10207
10207
|
});
|
|
@@ -10620,11 +10620,30 @@ function masked(expected, actual) {
|
|
|
10620
10620
|
const allSpelled = (value) => !PRINTED_DICTIONARY.test(value) && value.split("[").length === value.split(MASKED_BRACKET).length;
|
|
10621
10621
|
return `${expected}${actual}`.includes(MASKED_BRACKET) && allSpelled(expected) && allSpelled(actual);
|
|
10622
10622
|
}
|
|
10623
|
+
function orphansLeftOn(line) {
|
|
10624
|
+
const named = ORPHANS_LEFT.exec(line);
|
|
10625
|
+
if (named === null) {
|
|
10626
|
+
return null;
|
|
10627
|
+
}
|
|
10628
|
+
return { suite: named[1] ?? "", during: named[2] ?? "", count: Number(named[3]), nodes: named[4] ?? "" };
|
|
10629
|
+
}
|
|
10630
|
+
function orphansLeftSaid(left) {
|
|
10631
|
+
const nodes = left.count === 1 ? "1 node" : `${String(left.count)} nodes`;
|
|
10632
|
+
const hook = HOOKS_LEFT.get(left.during);
|
|
10633
|
+
const where = hook === undefined ? `${left.suite} > ${left.during}` : `${left.suite}'s ${hook}`;
|
|
10634
|
+
return `${where} left ${nodes} neither freed nor in the tree, which gdUnit4 counts as orphans: ${left.nodes}. Free each one, or make it with auto_free(), which frees it once the ${hook === undefined ? "case" : "suite"} is over.`;
|
|
10635
|
+
}
|
|
10623
10636
|
function orphansPrinted(printed) {
|
|
10624
10637
|
const suites = [];
|
|
10625
10638
|
let total = null;
|
|
10626
10639
|
let running = null;
|
|
10640
|
+
const named = new Map;
|
|
10627
10641
|
for (const line of printed) {
|
|
10642
|
+
const left = orphansLeftOn(line);
|
|
10643
|
+
if (left !== null) {
|
|
10644
|
+
named.set(left.suite, [...named.get(left.suite) ?? [], left]);
|
|
10645
|
+
continue;
|
|
10646
|
+
}
|
|
10628
10647
|
const started = RUNNING_SUITE.exec(line);
|
|
10629
10648
|
if (started?.[1] !== undefined) {
|
|
10630
10649
|
running = started[1];
|
|
@@ -10641,13 +10660,13 @@ function orphansPrinted(printed) {
|
|
|
10641
10660
|
continue;
|
|
10642
10661
|
}
|
|
10643
10662
|
if (running !== null && orphans > 0) {
|
|
10644
|
-
suites.push({ path: running, orphans });
|
|
10663
|
+
suites.push({ path: running, orphans, left: [] });
|
|
10645
10664
|
}
|
|
10646
10665
|
running = null;
|
|
10647
10666
|
}
|
|
10648
10667
|
return {
|
|
10649
10668
|
total: total ?? suites.reduce((sum, suite) => sum + suite.orphans, 0),
|
|
10650
|
-
suites
|
|
10669
|
+
suites: suites.map((suite) => ({ ...suite, left: named.get(suite.path) ?? [] }))
|
|
10651
10670
|
};
|
|
10652
10671
|
}
|
|
10653
10672
|
function finishesASuite(line) {
|
|
@@ -10694,7 +10713,7 @@ function testProgressMessage(printed, elapsedMs) {
|
|
|
10694
10713
|
const done = finished.length === 0 ? "No suite has finished" : `${finished.length === 1 ? "One suite has" : `${finished.length} suites have`} finished${failing === 0 ? "" : `, ${failing} with failures`}`;
|
|
10695
10714
|
return `${done}${running === null ? "" : `; running ${running}`}; ${took} in.`;
|
|
10696
10715
|
}
|
|
10697
|
-
var MalformedReportError, ENTITIES, ATTRIBUTE, HOOK_FAILURE, NOTHING_RAN, NO_SUCH_PATH, DISCOVERY_FAILED = "Script errors were detected during test discovery!", ESCAPE, LOCATION = `'[^'\\n]*' in \\S+:\\d+`, IGNORING_CASE = " \\(ignoring case\\)", PRINTED_ACTUAL, QUOTED_EQUALITY, COLOUR, SPELLED, BOTH_SIDES_ALIKE_NOTE = "[gdharness: the expected and the actual printed alike because gdUnit4 drops the marks that tell them apart when it writes its report, and the runner printed no copy of this one, so neither side above is the value the assertion found.]", VALUE_NOT_RECOVERED_NOTE = '[gdharness: the value after "but was" may not be the one the assertion found. gdUnit4 writes a failing string into its report as a character diff against the expected one with the marks dropped, which merges the two, and the runner printed no copy of this one to read the value back from.]', RENDERED_AWAY_NOTE = '[gdharness: the two sides print alike, so what tells them apart is something gdUnit4 does not print. The usual cause is BBCode in the expected string, which gdUnit4 renders as formatting instead of showing; the value after "but was" is the one the assertion found.]', MOST_REPORTS_SHOWN = 20, LONGEST_DETAIL = 20000, MASKED_BRACKET = "[lb]", PRINTED_DICTIONARY, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary", SUITE_COUNTS, CASE_FAILED;
|
|
10716
|
+
var MalformedReportError, ENTITIES, ATTRIBUTE, HOOK_FAILURE, NOTHING_RAN, NO_SUCH_PATH, DISCOVERY_FAILED = "Script errors were detected during test discovery!", ESCAPE, LOCATION = `'[^'\\n]*' in \\S+:\\d+`, IGNORING_CASE = " \\(ignoring case\\)", PRINTED_ACTUAL, QUOTED_EQUALITY, COLOUR, SPELLED, BOTH_SIDES_ALIKE_NOTE = "[gdharness: the expected and the actual printed alike because gdUnit4 drops the marks that tell them apart when it writes its report, and the runner printed no copy of this one, so neither side above is the value the assertion found.]", VALUE_NOT_RECOVERED_NOTE = '[gdharness: the value after "but was" may not be the one the assertion found. gdUnit4 writes a failing string into its report as a character diff against the expected one with the marks dropped, which merges the two, and the runner printed no copy of this one to read the value back from.]', RENDERED_AWAY_NOTE = '[gdharness: the two sides print alike, so what tells them apart is something gdUnit4 does not print. The usual cause is BBCode in the expected string, which gdUnit4 renders as formatting instead of showing; the value after "but was" is the one the assertion found.]', MOST_REPORTS_SHOWN = 20, LONGEST_DETAIL = 20000, MASKED_BRACKET = "[lb]", PRINTED_DICTIONARY, ORPHANS_LEFT, HOOKS_LEFT, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary", SUITE_COUNTS, CASE_FAILED;
|
|
10698
10717
|
var init_junit = __esm(() => {
|
|
10699
10718
|
MalformedReportError = class MalformedReportError extends Error {
|
|
10700
10719
|
constructor(message) {
|
|
@@ -10724,6 +10743,12 @@ var init_junit = __esm(() => {
|
|
|
10724
10743
|
"<DEL>": ""
|
|
10725
10744
|
};
|
|
10726
10745
|
PRINTED_DICTIONARY = /^\{\n\t[\s\S]*\n {2}\}$/;
|
|
10746
|
+
ORPHANS_LEFT = /^gdharness orphans: (res:\/\/.+?\.(?:gd|cs)) > (.+?) left (\d+): (.+?)\s*$/;
|
|
10747
|
+
HOOKS_LEFT = new Map([
|
|
10748
|
+
["before", "before()"],
|
|
10749
|
+
["after", "after()"],
|
|
10750
|
+
["before or after", "before() or after()"]
|
|
10751
|
+
]);
|
|
10727
10752
|
RUNNING_SUITE = /Run Test Suite:\s*(res:\/\/.+?\.(?:gd|cs))(?=\s|$)/;
|
|
10728
10753
|
ORPHANS = /(\d+)\s+orphans/;
|
|
10729
10754
|
SUITE_COUNTS = /(\d+)\s+test cases\s*\|\s*(\d+)\s+errors\s*\|\s*(\d+)\s+failures/;
|
|
@@ -10771,6 +10796,7 @@ function readRun(printed) {
|
|
|
10771
10796
|
afterCase = [];
|
|
10772
10797
|
};
|
|
10773
10798
|
const leftOut = new Map;
|
|
10799
|
+
const orphansLeft = [];
|
|
10774
10800
|
let engineVersion = null;
|
|
10775
10801
|
for (const [at, line] of lines.entries()) {
|
|
10776
10802
|
const banner = engineVersion === null ? BANNER.exec(line) : null;
|
|
@@ -10778,6 +10804,11 @@ function readRun(printed) {
|
|
|
10778
10804
|
engineVersion = banner[1] ?? null;
|
|
10779
10805
|
continue;
|
|
10780
10806
|
}
|
|
10807
|
+
const orphaned = orphansLeftOn(line);
|
|
10808
|
+
if (orphaned !== null) {
|
|
10809
|
+
orphansLeft.push(orphaned);
|
|
10810
|
+
continue;
|
|
10811
|
+
}
|
|
10781
10812
|
const left = LEFT_OUT.exec(line);
|
|
10782
10813
|
if (left !== null) {
|
|
10783
10814
|
const key = `${left[1] ?? ""} > ${left[2] ?? ""}`;
|
|
@@ -10864,7 +10895,19 @@ function readRun(printed) {
|
|
|
10864
10895
|
`);
|
|
10865
10896
|
const starved = STARVED.flatMap((pattern) => [...joined.matchAll(pattern)].map((found) => found[0]));
|
|
10866
10897
|
const unloadable = scriptErrorsPrinted(lines).map((error) => `${error.path}${error.line === null ? "" : `:${String(error.line)}`}: ${error.message}`);
|
|
10867
|
-
return {
|
|
10898
|
+
return {
|
|
10899
|
+
lines,
|
|
10900
|
+
suites,
|
|
10901
|
+
errors,
|
|
10902
|
+
leaks,
|
|
10903
|
+
starved,
|
|
10904
|
+
executed,
|
|
10905
|
+
unloadable,
|
|
10906
|
+
stoppedIn,
|
|
10907
|
+
leftOut,
|
|
10908
|
+
engineVersion,
|
|
10909
|
+
orphansLeft
|
|
10910
|
+
};
|
|
10868
10911
|
}
|
|
10869
10912
|
function withNestedErrors(printed, piped) {
|
|
10870
10913
|
const nested = piped.flatMap((entry, at) => {
|
|
@@ -10962,7 +11005,7 @@ function judgeEngine(printed, handed, end) {
|
|
|
10962
11005
|
const faults = [];
|
|
10963
11006
|
const failing = new Set;
|
|
10964
11007
|
const shownAt = new Set;
|
|
10965
|
-
const explained = end.code === GDUNIT_FAILED && [...printed.suites.values()].some((suite) => suite.cases.some((ran) => ran.verdict === "FAILED"));
|
|
11008
|
+
const explained = end.code === GDUNIT_FAILED && [...printed.suites.values()].some((suite) => suite.cases.some((ran) => ran.verdict === "FAILED")) || end.code === GDUNIT_ORPHANS && printed.orphansLeft.length > 0;
|
|
10966
11009
|
if (end.ended !== null) {
|
|
10967
11010
|
faults.push(end.ended);
|
|
10968
11011
|
} else if (end.code !== 0 && !explained) {
|
|
@@ -11027,6 +11070,10 @@ function judgeEngine(printed, handed, end) {
|
|
|
11027
11070
|
}
|
|
11028
11071
|
}
|
|
11029
11072
|
}
|
|
11073
|
+
for (const left of printed.orphansLeft) {
|
|
11074
|
+
faults.push(orphansLeftSaid(left));
|
|
11075
|
+
failing.add(left.suite);
|
|
11076
|
+
}
|
|
11030
11077
|
const unexpected = unexpectedErrors(printed, handed.expected, handed.suites.map((suite) => suite.path));
|
|
11031
11078
|
for (const error of unexpected.printed) {
|
|
11032
11079
|
faults.push(error.suite === null ? `an engine ${error.warning ? "warning" : "error"} outside every suite: ${error.text}` : `an engine ${error.warning ? "warning" : "error"} in ${error.suite} > ${error.during ?? ""}: ${error.text}; ${declaringAdvice(error.during ?? "", error.warning)}`);
|
|
@@ -11138,7 +11185,7 @@ ${String(error.warning)}`);
|
|
|
11138
11185
|
const neverPrinted = [...owed.values()].flatMap((debt) => debt.texts.map((text) => ({ suite: debt.suite, during: debt.during, text, warning: debt.warning })));
|
|
11139
11186
|
return { printed: unexpected, neverPrinted };
|
|
11140
11187
|
}
|
|
11141
|
-
var SUITE_OPENED, TIME, CASE_LINE, STATISTICS, ERROR_LINE, WARNING_LINE, EXECUTED, BANNER, LEFT_OUT, DRIVER_FALLBACK, MACHINE_WARNED, ORPHANS_REPORTED, STARVED, NESTED_ERROR = "While attempting to print an error, another error was printed:", WINDOWS_EXITS, MEMORY_EXITS, GDUNIT_FAILED = 100, GDUNIT_EXITS, GODOT_EXITS, TAIL = 60, MOST_NAMED = 8;
|
|
11188
|
+
var SUITE_OPENED, TIME, CASE_LINE, STATISTICS, ERROR_LINE, WARNING_LINE, EXECUTED, BANNER, LEFT_OUT, DRIVER_FALLBACK, MACHINE_WARNED, ORPHANS_REPORTED, STARVED, NESTED_ERROR = "While attempting to print an error, another error was printed:", WINDOWS_EXITS, MEMORY_EXITS, GDUNIT_FAILED = 100, GDUNIT_ORPHANS = 101, GDUNIT_EXITS, GODOT_EXITS, TAIL = 60, MOST_NAMED = 8;
|
|
11142
11189
|
var init_suite_judge = __esm(() => {
|
|
11143
11190
|
init_engine_leaks();
|
|
11144
11191
|
init_junit();
|
|
@@ -11170,7 +11217,7 @@ var init_suite_judge = __esm(() => {
|
|
|
11170
11217
|
MEMORY_EXITS = new Set([3221225495, 3221225773]);
|
|
11171
11218
|
GDUNIT_EXITS = new Map([
|
|
11172
11219
|
[GDUNIT_FAILED, "gdUnit4's code for a case that failed or errored"],
|
|
11173
|
-
[
|
|
11220
|
+
[GDUNIT_ORPHANS, "gdUnit4's code for orphan nodes left behind"],
|
|
11174
11221
|
[103, "gdUnit4's code for a suite that cannot run headless"],
|
|
11175
11222
|
[104, "gdUnit4's code for a Godot version it does not support"],
|
|
11176
11223
|
[105, "gdUnit4's code for scripts that did not compile"]
|
|
@@ -11476,13 +11523,295 @@ var init_engine_launch = __esm(() => {
|
|
|
11476
11523
|
MEMORY_PER_ENGINE = 1.5 * 1024 ** 3;
|
|
11477
11524
|
});
|
|
11478
11525
|
|
|
11526
|
+
// src/call-signal.ts
|
|
11527
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
11528
|
+
function stillGoingSaid(tool, op, elapsedMs) {
|
|
11529
|
+
const seconds = Math.floor(elapsedMs / 1000);
|
|
11530
|
+
const took = seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
11531
|
+
return `${op === null || op === "" ? tool : `${tool} ${op}`} is still going; ${took} in.`;
|
|
11532
|
+
}
|
|
11533
|
+
async function withCallSignal(signal, answer, progress, still) {
|
|
11534
|
+
if (progress === undefined) {
|
|
11535
|
+
return signal === undefined ? answer() : current.run({ signal, progress: undefined }, answer);
|
|
11536
|
+
}
|
|
11537
|
+
const began = Date.now();
|
|
11538
|
+
const rising = risingOnly(progress);
|
|
11539
|
+
let going = still;
|
|
11540
|
+
let quiet;
|
|
11541
|
+
const rearm = () => {
|
|
11542
|
+
clearTimeout(quiet);
|
|
11543
|
+
if (going === undefined) {
|
|
11544
|
+
return;
|
|
11545
|
+
}
|
|
11546
|
+
const { everyMs, said } = going;
|
|
11547
|
+
quiet = setTimeout(() => {
|
|
11548
|
+
const elapsed = Date.now() - began;
|
|
11549
|
+
told(Math.floor(elapsed / 1000), said(elapsed));
|
|
11550
|
+
}, everyMs);
|
|
11551
|
+
quiet.unref();
|
|
11552
|
+
};
|
|
11553
|
+
const told = (number, message) => {
|
|
11554
|
+
rising(number, message);
|
|
11555
|
+
rearm();
|
|
11556
|
+
};
|
|
11557
|
+
rearm();
|
|
11558
|
+
try {
|
|
11559
|
+
return await current.run({ signal, progress: told }, answer);
|
|
11560
|
+
} finally {
|
|
11561
|
+
going = undefined;
|
|
11562
|
+
clearTimeout(quiet);
|
|
11563
|
+
}
|
|
11564
|
+
}
|
|
11565
|
+
function risingOnly(progress) {
|
|
11566
|
+
let last = Number.NEGATIVE_INFINITY;
|
|
11567
|
+
return (number, message) => {
|
|
11568
|
+
last = number > last ? number : last + 1;
|
|
11569
|
+
progress(last, message);
|
|
11570
|
+
};
|
|
11571
|
+
}
|
|
11572
|
+
function callSignal() {
|
|
11573
|
+
return current.getStore()?.signal;
|
|
11574
|
+
}
|
|
11575
|
+
function callProgress() {
|
|
11576
|
+
return current.getStore()?.progress;
|
|
11577
|
+
}
|
|
11578
|
+
var current, CALL_HEARTBEAT_MS = 20000;
|
|
11579
|
+
var init_call_signal = __esm(() => {
|
|
11580
|
+
current = new AsyncLocalStorage;
|
|
11581
|
+
});
|
|
11582
|
+
|
|
11583
|
+
// src/engine-slots.ts
|
|
11584
|
+
import { closeSync as closeSync3, mkdirSync as mkdirSync3, openSync as openSync3, readFileSync as readFileSync7, rmSync as rmSync5, statSync as statSync6, writeSync as writeSync2 } from "node:fs";
|
|
11585
|
+
import { availableParallelism as availableParallelism2, homedir as homedir2 } from "node:os";
|
|
11586
|
+
import { join as join8, resolve as resolve4 } from "node:path";
|
|
11587
|
+
import process7 from "node:process";
|
|
11588
|
+
import { setTimeout as delay3 } from "node:timers/promises";
|
|
11589
|
+
function machineSlots(asked = {
|
|
11590
|
+
directory: null,
|
|
11591
|
+
count: null
|
|
11592
|
+
}, machine = {
|
|
11593
|
+
env: process7.env,
|
|
11594
|
+
platform: process7.platform,
|
|
11595
|
+
processors: availableParallelism2(),
|
|
11596
|
+
home: homedir2(),
|
|
11597
|
+
cwd: process7.cwd()
|
|
11598
|
+
}) {
|
|
11599
|
+
const { env } = machine;
|
|
11600
|
+
const named = nonEmpty(env[SLOTS_VARIABLE]);
|
|
11601
|
+
const xdg = nonEmpty(env["XDG_CACHE_HOME"]);
|
|
11602
|
+
const local = machine.platform === "win32" ? nonEmpty(env["LOCALAPPDATA"]) : undefined;
|
|
11603
|
+
const cache = join8(xdg ?? local ?? join8(machine.home, ".cache"), "gdharness");
|
|
11604
|
+
const directory = asked.directory ?? (named === undefined ? join8(cache, "slots") : resolve4(machine.cwd, named));
|
|
11605
|
+
const counted = nonEmpty(env[SLOT_COUNT_VARIABLE]);
|
|
11606
|
+
let count = asked.count;
|
|
11607
|
+
if (count === null && counted !== undefined) {
|
|
11608
|
+
count = Number(counted);
|
|
11609
|
+
if (!Number.isInteger(count) || count < 1) {
|
|
11610
|
+
throw new EngineUsageError(`${SLOT_COUNT_VARIABLE} takes a whole number above 0, not ${counted}.`);
|
|
11611
|
+
}
|
|
11612
|
+
}
|
|
11613
|
+
return { directory, count: count ?? Math.max(1, Math.floor(machine.processors / PROCESSORS_A_SLOT)) };
|
|
11614
|
+
}
|
|
11615
|
+
function nonEmpty(value) {
|
|
11616
|
+
return value === undefined || value === "" ? undefined : value;
|
|
11617
|
+
}
|
|
11618
|
+
function holderOf(content) {
|
|
11619
|
+
try {
|
|
11620
|
+
const parsed = JSON.parse(content);
|
|
11621
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
11622
|
+
return null;
|
|
11623
|
+
}
|
|
11624
|
+
const { pid, started } = parsed;
|
|
11625
|
+
return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && typeof started === "number" ? { pid, started } : null;
|
|
11626
|
+
} catch {
|
|
11627
|
+
return null;
|
|
11628
|
+
}
|
|
11629
|
+
}
|
|
11630
|
+
function lockIsFree(content, writtenAt, now, isAlive, startOf) {
|
|
11631
|
+
const holder = holderOf(content);
|
|
11632
|
+
if (holder === null) {
|
|
11633
|
+
return now - writtenAt > UNWRITTEN_MS;
|
|
11634
|
+
}
|
|
11635
|
+
if (!isAlive(holder.pid)) {
|
|
11636
|
+
return true;
|
|
11637
|
+
}
|
|
11638
|
+
const started = startOf(holder.pid);
|
|
11639
|
+
return started !== undefined && Math.abs(started - holder.started) > START_TOLERANCE_MS;
|
|
11640
|
+
}
|
|
11641
|
+
async function ownStart() {
|
|
11642
|
+
const said = (await startTimesOf([process7.pid])).get(process7.pid);
|
|
11643
|
+
return said ?? Math.round(Date.now() - process7.uptime() * 1000);
|
|
11644
|
+
}
|
|
11645
|
+
|
|
11646
|
+
class EngineSlots {
|
|
11647
|
+
starts = new Map;
|
|
11648
|
+
held = new Set;
|
|
11649
|
+
saidAt = Number.NEGATIVE_INFINITY;
|
|
11650
|
+
directory;
|
|
11651
|
+
count;
|
|
11652
|
+
holder;
|
|
11653
|
+
say;
|
|
11654
|
+
constructor(directory, count, holder, say) {
|
|
11655
|
+
this.directory = directory;
|
|
11656
|
+
this.count = count;
|
|
11657
|
+
this.holder = holder;
|
|
11658
|
+
this.say = say;
|
|
11659
|
+
}
|
|
11660
|
+
async take(signal, waiting) {
|
|
11661
|
+
mkdirSync3(this.directory, { recursive: true });
|
|
11662
|
+
const began = Date.now();
|
|
11663
|
+
let said = Number.NEGATIVE_INFINITY;
|
|
11664
|
+
for (;; ) {
|
|
11665
|
+
signal?.throwIfAborted();
|
|
11666
|
+
for (let slot = 0;slot < this.count; slot += 1) {
|
|
11667
|
+
const lock = join8(this.directory, `${String(slot)}.lock`);
|
|
11668
|
+
if (this.made(lock) || await this.takenOver(lock)) {
|
|
11669
|
+
this.held.add(lock);
|
|
11670
|
+
return lock;
|
|
11671
|
+
}
|
|
11672
|
+
}
|
|
11673
|
+
const now = Date.now();
|
|
11674
|
+
if (waiting !== undefined) {
|
|
11675
|
+
if (now - said >= WAIT_SAID_MS) {
|
|
11676
|
+
said = now;
|
|
11677
|
+
waiting(now - began);
|
|
11678
|
+
}
|
|
11679
|
+
} else if (this.say !== undefined && this.held.size === 0 && now - began >= COMMAND_WAIT_SAID_MS && now - this.saidAt >= WAIT_SAID_MS) {
|
|
11680
|
+
this.saidAt = now;
|
|
11681
|
+
this.say(slotWaitSaid(now - began, this.count, this.directory));
|
|
11682
|
+
}
|
|
11683
|
+
await delay3(SLOT_WAIT_MS, undefined, signal === undefined ? {} : { signal });
|
|
11684
|
+
}
|
|
11685
|
+
}
|
|
11686
|
+
releaseAll() {
|
|
11687
|
+
for (const lock of this.held) {
|
|
11688
|
+
this.release(lock);
|
|
11689
|
+
}
|
|
11690
|
+
}
|
|
11691
|
+
release(lock) {
|
|
11692
|
+
this.held.delete(lock);
|
|
11693
|
+
try {
|
|
11694
|
+
if (holderOf(readFileSync7(lock, "utf8"))?.pid === this.holder.pid) {
|
|
11695
|
+
rmSync5(lock, { force: true });
|
|
11696
|
+
}
|
|
11697
|
+
} catch {}
|
|
11698
|
+
}
|
|
11699
|
+
made(path) {
|
|
11700
|
+
let made;
|
|
11701
|
+
try {
|
|
11702
|
+
made = openSync3(path, "wx");
|
|
11703
|
+
} catch {
|
|
11704
|
+
return false;
|
|
11705
|
+
}
|
|
11706
|
+
try {
|
|
11707
|
+
writeSync2(made, JSON.stringify(this.holder));
|
|
11708
|
+
} finally {
|
|
11709
|
+
closeSync3(made);
|
|
11710
|
+
}
|
|
11711
|
+
return true;
|
|
11712
|
+
}
|
|
11713
|
+
async takenOver(lock) {
|
|
11714
|
+
if (!await this.free(lock)) {
|
|
11715
|
+
return false;
|
|
11716
|
+
}
|
|
11717
|
+
const steal = `${lock}.steal`;
|
|
11718
|
+
if (!this.made(steal)) {
|
|
11719
|
+
try {
|
|
11720
|
+
if (Date.now() - statSync6(steal).mtimeMs > UNWRITTEN_MS) {
|
|
11721
|
+
rmSync5(steal, { force: true });
|
|
11722
|
+
}
|
|
11723
|
+
} catch {}
|
|
11724
|
+
return false;
|
|
11725
|
+
}
|
|
11726
|
+
try {
|
|
11727
|
+
if (!await this.free(lock)) {
|
|
11728
|
+
return false;
|
|
11729
|
+
}
|
|
11730
|
+
try {
|
|
11731
|
+
rmSync5(lock, { force: true });
|
|
11732
|
+
} catch {
|
|
11733
|
+
return false;
|
|
11734
|
+
}
|
|
11735
|
+
return this.made(lock);
|
|
11736
|
+
} finally {
|
|
11737
|
+
rmSync5(steal, { force: true });
|
|
11738
|
+
}
|
|
11739
|
+
}
|
|
11740
|
+
async free(lock) {
|
|
11741
|
+
let content;
|
|
11742
|
+
let writtenAt;
|
|
11743
|
+
try {
|
|
11744
|
+
content = readFileSync7(lock, "utf8");
|
|
11745
|
+
writtenAt = statSync6(lock).mtimeMs;
|
|
11746
|
+
} catch {
|
|
11747
|
+
return false;
|
|
11748
|
+
}
|
|
11749
|
+
const pid = holderOf(content)?.pid;
|
|
11750
|
+
if (pid !== undefined && alive(pid)) {
|
|
11751
|
+
await this.readStart(pid);
|
|
11752
|
+
}
|
|
11753
|
+
return lockIsFree(content, writtenAt, Date.now(), alive, (asked) => this.starts.get(asked)?.started);
|
|
11754
|
+
}
|
|
11755
|
+
async readStart(pid) {
|
|
11756
|
+
const known = this.starts.get(pid);
|
|
11757
|
+
if (known !== undefined && Date.now() - known.readAt < START_READ_MS) {
|
|
11758
|
+
return;
|
|
11759
|
+
}
|
|
11760
|
+
const started = (await startTimesOf([pid])).get(pid);
|
|
11761
|
+
this.starts.set(pid, { started, readAt: Date.now() });
|
|
11762
|
+
}
|
|
11763
|
+
}
|
|
11764
|
+
function sayWaitsTo(say) {
|
|
11765
|
+
saidTo = say;
|
|
11766
|
+
}
|
|
11767
|
+
function processSlots() {
|
|
11768
|
+
ours ??= ownStart().then((started) => {
|
|
11769
|
+
const chosen = machineSlots();
|
|
11770
|
+
return new EngineSlots(chosen.directory, chosen.count, { pid: process7.pid, started }, (line) => {
|
|
11771
|
+
saidTo?.(line);
|
|
11772
|
+
});
|
|
11773
|
+
});
|
|
11774
|
+
return ours;
|
|
11775
|
+
}
|
|
11776
|
+
function slotWaitSaid(waitedMs, count, directory) {
|
|
11777
|
+
const slots = count === 1 ? `the one engine slot on this machine (${directory}) is in use` : `all ${String(count)} engine slots on this machine (${directory}) are in use`;
|
|
11778
|
+
return `Waiting${waitedMs >= 1000 ? ` ${String(Math.round(waitedMs / 1000))} s so far` : ""} for an engine: ${slots}, by a gate or another tool, and the engine starts when one is given back.`;
|
|
11779
|
+
}
|
|
11780
|
+
async function slotForCall() {
|
|
11781
|
+
const slots = await processSlots();
|
|
11782
|
+
const progress = callProgress();
|
|
11783
|
+
const lock = await slots.take(callSignal(), progress === undefined ? undefined : (waitedMs) => {
|
|
11784
|
+
progress(waitedMs / 1000, slotWaitSaid(waitedMs, slots.count, slots.directory));
|
|
11785
|
+
});
|
|
11786
|
+
return {
|
|
11787
|
+
release: () => {
|
|
11788
|
+
slots.release(lock);
|
|
11789
|
+
}
|
|
11790
|
+
};
|
|
11791
|
+
}
|
|
11792
|
+
async function inASlot(body) {
|
|
11793
|
+
const held = await slotForCall();
|
|
11794
|
+
try {
|
|
11795
|
+
return await body();
|
|
11796
|
+
} finally {
|
|
11797
|
+
held.release();
|
|
11798
|
+
}
|
|
11799
|
+
}
|
|
11800
|
+
var SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 5, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4, SLOT_WAIT_MS = 50, WAIT_SAID_MS = 20000, COMMAND_WAIT_SAID_MS = 2000, START_READ_MS = 1e4, ours, saidTo;
|
|
11801
|
+
var init_engine_slots = __esm(() => {
|
|
11802
|
+
init_alive();
|
|
11803
|
+
init_call_signal();
|
|
11804
|
+
init_engine_launch();
|
|
11805
|
+
init_process_children();
|
|
11806
|
+
});
|
|
11807
|
+
|
|
11479
11808
|
// src/detection.ts
|
|
11480
|
-
import { existsSync as existsSync6, readdirSync as readdirSync4, statSync as
|
|
11481
|
-
import { homedir as
|
|
11482
|
-
import { join as
|
|
11809
|
+
import { existsSync as existsSync6, readdirSync as readdirSync4, statSync as statSync7 } from "node:fs";
|
|
11810
|
+
import { homedir as homedir3 } from "node:os";
|
|
11811
|
+
import { join as join9 } from "node:path";
|
|
11483
11812
|
function resolveHomeDirectory() {
|
|
11484
11813
|
try {
|
|
11485
|
-
return
|
|
11814
|
+
return homedir3();
|
|
11486
11815
|
} catch {
|
|
11487
11816
|
return "";
|
|
11488
11817
|
}
|
|
@@ -11503,16 +11832,16 @@ function scanDirectoryForGodotBinaries(directory, platform) {
|
|
|
11503
11832
|
if (!pattern.test(name)) {
|
|
11504
11833
|
continue;
|
|
11505
11834
|
}
|
|
11506
|
-
const fullPath =
|
|
11835
|
+
const fullPath = join9(directory, name);
|
|
11507
11836
|
try {
|
|
11508
|
-
const stat =
|
|
11837
|
+
const stat = statSync7(fullPath);
|
|
11509
11838
|
if (stat.isFile()) {
|
|
11510
11839
|
matches.push({ name, mtime: stat.mtimeMs });
|
|
11511
11840
|
}
|
|
11512
11841
|
} catch {}
|
|
11513
11842
|
}
|
|
11514
11843
|
matches.sort((a, b) => b.mtime - a.mtime);
|
|
11515
|
-
return matches.map((m) =>
|
|
11844
|
+
return matches.map((m) => join9(directory, m.name));
|
|
11516
11845
|
}
|
|
11517
11846
|
function conventionalPaths(platform, home) {
|
|
11518
11847
|
const paths = ["godot"];
|
|
@@ -11719,7 +12048,7 @@ var init_launch = __esm(() => {
|
|
|
11719
12048
|
|
|
11720
12049
|
// src/godot-path.ts
|
|
11721
12050
|
import { existsSync as existsSync7 } from "node:fs";
|
|
11722
|
-
import { isAbsolute as isAbsolute4, normalize, resolve as
|
|
12051
|
+
import { isAbsolute as isAbsolute4, normalize, resolve as resolve5 } from "node:path";
|
|
11723
12052
|
|
|
11724
12053
|
class GodotLocator {
|
|
11725
12054
|
answered = new Map;
|
|
@@ -11731,7 +12060,7 @@ class GodotLocator {
|
|
|
11731
12060
|
this.found = null;
|
|
11732
12061
|
const named = envValue("GODOT_PATH");
|
|
11733
12062
|
if (named !== undefined) {
|
|
11734
|
-
const path = named === "godot" || isAbsolute4(named) ? named :
|
|
12063
|
+
const path = named === "godot" || isAbsolute4(named) ? named : resolve5(named);
|
|
11735
12064
|
const reason = await this.whyNot(path);
|
|
11736
12065
|
if (reason === null) {
|
|
11737
12066
|
this.found = path;
|
|
@@ -11801,274 +12130,6 @@ function dictionary(entries) {
|
|
|
11801
12130
|
return Object.assign(emptyRecord(), entries);
|
|
11802
12131
|
}
|
|
11803
12132
|
|
|
11804
|
-
// src/call-signal.ts
|
|
11805
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
11806
|
-
function stillGoingSaid(tool, op, elapsedMs) {
|
|
11807
|
-
const seconds = Math.floor(elapsedMs / 1000);
|
|
11808
|
-
const took = seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
11809
|
-
return `${op === null || op === "" ? tool : `${tool} ${op}`} is still going; ${took} in.`;
|
|
11810
|
-
}
|
|
11811
|
-
async function withCallSignal(signal, answer, progress, still) {
|
|
11812
|
-
if (progress === undefined) {
|
|
11813
|
-
return signal === undefined ? answer() : current.run({ signal, progress: undefined }, answer);
|
|
11814
|
-
}
|
|
11815
|
-
const began = Date.now();
|
|
11816
|
-
const rising = risingOnly(progress);
|
|
11817
|
-
let going = still;
|
|
11818
|
-
let quiet;
|
|
11819
|
-
const rearm = () => {
|
|
11820
|
-
clearTimeout(quiet);
|
|
11821
|
-
if (going === undefined) {
|
|
11822
|
-
return;
|
|
11823
|
-
}
|
|
11824
|
-
const { everyMs, said } = going;
|
|
11825
|
-
quiet = setTimeout(() => {
|
|
11826
|
-
const elapsed = Date.now() - began;
|
|
11827
|
-
told(Math.floor(elapsed / 1000), said(elapsed));
|
|
11828
|
-
}, everyMs);
|
|
11829
|
-
quiet.unref();
|
|
11830
|
-
};
|
|
11831
|
-
const told = (number, message) => {
|
|
11832
|
-
rising(number, message);
|
|
11833
|
-
rearm();
|
|
11834
|
-
};
|
|
11835
|
-
rearm();
|
|
11836
|
-
try {
|
|
11837
|
-
return await current.run({ signal, progress: told }, answer);
|
|
11838
|
-
} finally {
|
|
11839
|
-
going = undefined;
|
|
11840
|
-
clearTimeout(quiet);
|
|
11841
|
-
}
|
|
11842
|
-
}
|
|
11843
|
-
function risingOnly(progress) {
|
|
11844
|
-
let last = Number.NEGATIVE_INFINITY;
|
|
11845
|
-
return (number, message) => {
|
|
11846
|
-
last = number > last ? number : last + 1;
|
|
11847
|
-
progress(last, message);
|
|
11848
|
-
};
|
|
11849
|
-
}
|
|
11850
|
-
function callSignal() {
|
|
11851
|
-
return current.getStore()?.signal;
|
|
11852
|
-
}
|
|
11853
|
-
function callProgress() {
|
|
11854
|
-
return current.getStore()?.progress;
|
|
11855
|
-
}
|
|
11856
|
-
var current, CALL_HEARTBEAT_MS = 20000;
|
|
11857
|
-
var init_call_signal = __esm(() => {
|
|
11858
|
-
current = new AsyncLocalStorage;
|
|
11859
|
-
});
|
|
11860
|
-
|
|
11861
|
-
// src/engine-slots.ts
|
|
11862
|
-
import { closeSync as closeSync3, mkdirSync as mkdirSync3, openSync as openSync3, readFileSync as readFileSync7, rmSync as rmSync5, statSync as statSync7, writeSync as writeSync2 } from "node:fs";
|
|
11863
|
-
import { availableParallelism as availableParallelism2, homedir as homedir3 } from "node:os";
|
|
11864
|
-
import { join as join9, resolve as resolve5 } from "node:path";
|
|
11865
|
-
import process8 from "node:process";
|
|
11866
|
-
import { setTimeout as delay3 } from "node:timers/promises";
|
|
11867
|
-
function machineSlots(asked = {
|
|
11868
|
-
directory: null,
|
|
11869
|
-
count: null
|
|
11870
|
-
}, machine = {
|
|
11871
|
-
env: process8.env,
|
|
11872
|
-
platform: process8.platform,
|
|
11873
|
-
processors: availableParallelism2(),
|
|
11874
|
-
home: homedir3(),
|
|
11875
|
-
cwd: process8.cwd()
|
|
11876
|
-
}) {
|
|
11877
|
-
const { env } = machine;
|
|
11878
|
-
const named = nonEmpty(env[SLOTS_VARIABLE]);
|
|
11879
|
-
const xdg = nonEmpty(env["XDG_CACHE_HOME"]);
|
|
11880
|
-
const local = machine.platform === "win32" ? nonEmpty(env["LOCALAPPDATA"]) : undefined;
|
|
11881
|
-
const cache = join9(xdg ?? local ?? join9(machine.home, ".cache"), "gdharness");
|
|
11882
|
-
const directory = asked.directory ?? (named === undefined ? join9(cache, "slots") : resolve5(machine.cwd, named));
|
|
11883
|
-
const counted = nonEmpty(env[SLOT_COUNT_VARIABLE]);
|
|
11884
|
-
let count = asked.count;
|
|
11885
|
-
if (count === null && counted !== undefined) {
|
|
11886
|
-
count = Number(counted);
|
|
11887
|
-
if (!Number.isInteger(count) || count < 1) {
|
|
11888
|
-
throw new EngineUsageError(`${SLOT_COUNT_VARIABLE} takes a whole number above 0, not ${counted}.`);
|
|
11889
|
-
}
|
|
11890
|
-
}
|
|
11891
|
-
return { directory, count: count ?? Math.max(1, Math.floor(machine.processors / PROCESSORS_A_SLOT)) };
|
|
11892
|
-
}
|
|
11893
|
-
function nonEmpty(value) {
|
|
11894
|
-
return value === undefined || value === "" ? undefined : value;
|
|
11895
|
-
}
|
|
11896
|
-
function holderOf(content) {
|
|
11897
|
-
try {
|
|
11898
|
-
const parsed = JSON.parse(content);
|
|
11899
|
-
if (typeof parsed !== "object" || parsed === null) {
|
|
11900
|
-
return null;
|
|
11901
|
-
}
|
|
11902
|
-
const { pid, started } = parsed;
|
|
11903
|
-
return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && typeof started === "number" ? { pid, started } : null;
|
|
11904
|
-
} catch {
|
|
11905
|
-
return null;
|
|
11906
|
-
}
|
|
11907
|
-
}
|
|
11908
|
-
function lockIsFree(content, writtenAt, now, isAlive, startOf) {
|
|
11909
|
-
const holder = holderOf(content);
|
|
11910
|
-
if (holder === null) {
|
|
11911
|
-
return now - writtenAt > UNWRITTEN_MS;
|
|
11912
|
-
}
|
|
11913
|
-
if (!isAlive(holder.pid)) {
|
|
11914
|
-
return true;
|
|
11915
|
-
}
|
|
11916
|
-
const started = startOf(holder.pid);
|
|
11917
|
-
return started !== undefined && Math.abs(started - holder.started) > START_TOLERANCE_MS;
|
|
11918
|
-
}
|
|
11919
|
-
async function ownStart() {
|
|
11920
|
-
const said = (await startTimesOf([process8.pid])).get(process8.pid);
|
|
11921
|
-
return said ?? Math.round(Date.now() - process8.uptime() * 1000);
|
|
11922
|
-
}
|
|
11923
|
-
|
|
11924
|
-
class EngineSlots {
|
|
11925
|
-
starts = new Map;
|
|
11926
|
-
held = new Set;
|
|
11927
|
-
directory;
|
|
11928
|
-
count;
|
|
11929
|
-
holder;
|
|
11930
|
-
constructor(directory, count, holder) {
|
|
11931
|
-
this.directory = directory;
|
|
11932
|
-
this.count = count;
|
|
11933
|
-
this.holder = holder;
|
|
11934
|
-
}
|
|
11935
|
-
async take(signal, waiting) {
|
|
11936
|
-
mkdirSync3(this.directory, { recursive: true });
|
|
11937
|
-
const began = Date.now();
|
|
11938
|
-
let said = Number.NEGATIVE_INFINITY;
|
|
11939
|
-
for (;; ) {
|
|
11940
|
-
signal?.throwIfAborted();
|
|
11941
|
-
for (let slot = 0;slot < this.count; slot += 1) {
|
|
11942
|
-
const lock = join9(this.directory, `${String(slot)}.lock`);
|
|
11943
|
-
if (this.made(lock) || await this.takenOver(lock)) {
|
|
11944
|
-
this.held.add(lock);
|
|
11945
|
-
return lock;
|
|
11946
|
-
}
|
|
11947
|
-
}
|
|
11948
|
-
if (waiting !== undefined && Date.now() - said >= WAIT_SAID_MS) {
|
|
11949
|
-
said = Date.now();
|
|
11950
|
-
waiting(said - began);
|
|
11951
|
-
}
|
|
11952
|
-
await delay3(SLOT_WAIT_MS, undefined, signal === undefined ? {} : { signal });
|
|
11953
|
-
}
|
|
11954
|
-
}
|
|
11955
|
-
releaseAll() {
|
|
11956
|
-
for (const lock of this.held) {
|
|
11957
|
-
this.release(lock);
|
|
11958
|
-
}
|
|
11959
|
-
}
|
|
11960
|
-
release(lock) {
|
|
11961
|
-
this.held.delete(lock);
|
|
11962
|
-
try {
|
|
11963
|
-
if (holderOf(readFileSync7(lock, "utf8"))?.pid === this.holder.pid) {
|
|
11964
|
-
rmSync5(lock, { force: true });
|
|
11965
|
-
}
|
|
11966
|
-
} catch {}
|
|
11967
|
-
}
|
|
11968
|
-
made(path) {
|
|
11969
|
-
let made;
|
|
11970
|
-
try {
|
|
11971
|
-
made = openSync3(path, "wx");
|
|
11972
|
-
} catch {
|
|
11973
|
-
return false;
|
|
11974
|
-
}
|
|
11975
|
-
try {
|
|
11976
|
-
writeSync2(made, JSON.stringify(this.holder));
|
|
11977
|
-
} finally {
|
|
11978
|
-
closeSync3(made);
|
|
11979
|
-
}
|
|
11980
|
-
return true;
|
|
11981
|
-
}
|
|
11982
|
-
async takenOver(lock) {
|
|
11983
|
-
if (!await this.free(lock)) {
|
|
11984
|
-
return false;
|
|
11985
|
-
}
|
|
11986
|
-
const steal = `${lock}.steal`;
|
|
11987
|
-
if (!this.made(steal)) {
|
|
11988
|
-
try {
|
|
11989
|
-
if (Date.now() - statSync7(steal).mtimeMs > UNWRITTEN_MS) {
|
|
11990
|
-
rmSync5(steal, { force: true });
|
|
11991
|
-
}
|
|
11992
|
-
} catch {}
|
|
11993
|
-
return false;
|
|
11994
|
-
}
|
|
11995
|
-
try {
|
|
11996
|
-
if (!await this.free(lock)) {
|
|
11997
|
-
return false;
|
|
11998
|
-
}
|
|
11999
|
-
try {
|
|
12000
|
-
rmSync5(lock, { force: true });
|
|
12001
|
-
} catch {
|
|
12002
|
-
return false;
|
|
12003
|
-
}
|
|
12004
|
-
return this.made(lock);
|
|
12005
|
-
} finally {
|
|
12006
|
-
rmSync5(steal, { force: true });
|
|
12007
|
-
}
|
|
12008
|
-
}
|
|
12009
|
-
async free(lock) {
|
|
12010
|
-
let content;
|
|
12011
|
-
let writtenAt;
|
|
12012
|
-
try {
|
|
12013
|
-
content = readFileSync7(lock, "utf8");
|
|
12014
|
-
writtenAt = statSync7(lock).mtimeMs;
|
|
12015
|
-
} catch {
|
|
12016
|
-
return false;
|
|
12017
|
-
}
|
|
12018
|
-
const pid = holderOf(content)?.pid;
|
|
12019
|
-
if (pid !== undefined && alive(pid)) {
|
|
12020
|
-
await this.readStart(pid);
|
|
12021
|
-
}
|
|
12022
|
-
return lockIsFree(content, writtenAt, Date.now(), alive, (asked) => this.starts.get(asked)?.started);
|
|
12023
|
-
}
|
|
12024
|
-
async readStart(pid) {
|
|
12025
|
-
const known = this.starts.get(pid);
|
|
12026
|
-
if (known !== undefined && Date.now() - known.readAt < START_READ_MS) {
|
|
12027
|
-
return;
|
|
12028
|
-
}
|
|
12029
|
-
const started = (await startTimesOf([pid])).get(pid);
|
|
12030
|
-
this.starts.set(pid, { started, readAt: Date.now() });
|
|
12031
|
-
}
|
|
12032
|
-
}
|
|
12033
|
-
function processSlots() {
|
|
12034
|
-
ours ??= ownStart().then((started) => {
|
|
12035
|
-
const chosen = machineSlots();
|
|
12036
|
-
return new EngineSlots(chosen.directory, chosen.count, { pid: process8.pid, started });
|
|
12037
|
-
});
|
|
12038
|
-
return ours;
|
|
12039
|
-
}
|
|
12040
|
-
function slotWaitSaid(waitedMs, count, directory) {
|
|
12041
|
-
const slots = count === 1 ? `the one engine slot on this machine (${directory}) is in use` : `all ${String(count)} engine slots on this machine (${directory}) are in use`;
|
|
12042
|
-
return `Waiting${waitedMs >= 1000 ? ` ${String(Math.round(waitedMs / 1000))} s so far` : ""} for an engine: ${slots}, by a gate or another tool, and the engine starts when one is given back.`;
|
|
12043
|
-
}
|
|
12044
|
-
async function slotForCall() {
|
|
12045
|
-
const slots = await processSlots();
|
|
12046
|
-
const progress = callProgress();
|
|
12047
|
-
const lock = await slots.take(callSignal(), progress === undefined ? undefined : (waitedMs) => {
|
|
12048
|
-
progress(waitedMs / 1000, slotWaitSaid(waitedMs, slots.count, slots.directory));
|
|
12049
|
-
});
|
|
12050
|
-
return {
|
|
12051
|
-
release: () => {
|
|
12052
|
-
slots.release(lock);
|
|
12053
|
-
}
|
|
12054
|
-
};
|
|
12055
|
-
}
|
|
12056
|
-
async function inASlot(body) {
|
|
12057
|
-
const held = await slotForCall();
|
|
12058
|
-
try {
|
|
12059
|
-
return await body();
|
|
12060
|
-
} finally {
|
|
12061
|
-
held.release();
|
|
12062
|
-
}
|
|
12063
|
-
}
|
|
12064
|
-
var SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 5, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4, SLOT_WAIT_MS = 50, WAIT_SAID_MS = 20000, START_READ_MS = 1e4, ours;
|
|
12065
|
-
var init_engine_slots = __esm(() => {
|
|
12066
|
-
init_alive();
|
|
12067
|
-
init_call_signal();
|
|
12068
|
-
init_engine_launch();
|
|
12069
|
-
init_process_children();
|
|
12070
|
-
});
|
|
12071
|
-
|
|
12072
12133
|
// src/game-log.ts
|
|
12073
12134
|
import { StringDecoder } from "node:string_decoder";
|
|
12074
12135
|
function announced(line) {
|
|
@@ -16407,7 +16468,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16407
16468
|
title: "Run tests",
|
|
16408
16469
|
effect: "additive",
|
|
16409
16470
|
description: "Runs the project's gdUnit4 tests and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, its reports under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. Identical reports fold into one with times, at most twenty different ones are kept (reportsLeftOut counts the rest), and past about a megabyte the largest cases keep only their first message (failedWithoutDetail). failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted, not listed, and an engine message keeps the frames above gdUnit4, not the twenty inside it; identical engine messages come back once under engineEntries, with times saying how often they were printed. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes. What changed on disk since it was imported is then imported before any suite runs, and named under importedFirst. On Windows and Linux the run gets a user:// of its own, away from a player's saves; on macOS the engine reads user:// off HOME, so it writes where the player keeps theirs, and savesNote says so. The run is headless unless a suite in it needs a window as gdharness test reads it, and then has one, on a desktop of its own on Windows. A run that found nothing to run is never called a pass, and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. An autoload that does not compile stops the run at once, its errors listed the same way. A client sending a progress token is told as each suite finishes, and at least every twenty seconds. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but orphan nodes were left comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and is not listed there, and none are when the project turns off gdUnit4's report/godot/script_error setting; a suite declares them all the same. Needs gdUnit4 under addons/gdUnit4.",
|
|
16410
|
-
answer: "passed is the verdict to read. exitCode is the engine's own exit, gdUnit4's code for what it saw, and is 0 beside passed false when the run is failed for something gdUnit4 does not judge, such as an engine error no suite declared. time is gdUnit4's own sum of the seconds its cases took, from its report. tookMs is the whole call in milliseconds, from the class rebuild to the engine's exit, which is what a caller waits for, and tookMsBy splits it into the class rebuild (classes), the import after it (import) and the engine running the suites (engine), each with any wait for an engine slot or the editor's scan in it. The run is judged as gdharness test judges it: undeclaredErrors names each engine error that no suite declared in its GDHARNESS_EXPECTED_ERRORS constant, and each engine warning a suite printed and did not declare in its GDHARNESS_EXPECTED_WARNINGS, marked warning true, with the suite and the case that printed it, and declaredNotPrinted each declared error or warning its case did not print. A declared text must be the whole line after ERROR: or WARNING: and its space, so a longer one its case printed holding the text is named under printedInstead. Either fails the run, and each suite holding one is listed with its count. leakedAtExit holds the engine's lines saying it exited still holding objects, resources or
|
|
16471
|
+
answer: "passed is the verdict to read. exitCode is the engine's own exit, gdUnit4's code for what it saw, and is 0 beside passed false when the run is failed for something gdUnit4 does not judge, such as an engine error no suite declared. time is gdUnit4's own sum of the seconds its cases took, from its report. tookMs is the whole call in milliseconds, from the class rebuild to the engine's exit, which is what a caller waits for, and tookMsBy splits it into the class rebuild (classes), the import after it (import) and the engine running the suites (engine), each with any wait for an engine slot or the editor's scan in it. The run is judged as gdharness test judges it: undeclaredErrors names each engine error that no suite declared in its GDHARNESS_EXPECTED_ERRORS constant, and each engine warning a suite printed and did not declare in its GDHARNESS_EXPECTED_WARNINGS, marked warning true, with the suite and the case that printed it, and declaredNotPrinted each declared error or warning its case did not print. A declared text must be the whole line after ERROR: or WARNING: and its space, so a longer one its case printed holding the text is named under printedInstead. Either fails the run, and each suite holding one is listed with its count. leakedAtExit holds the engine's lines saying it exited still holding objects, resources, memory pages or a server's RIDs, which fail the run too, and the note names any script of the project Godot itself never frees. engineErrors counts every engine error the run printed but those a suite declared. orphans totals the orphan nodes gdUnit4 counted, and each suite that left some has its count, and a warning saying how to free them, under warnings, with left naming each case that left some (before or after for one of the suite's hooks, or the one value \"before or after\" where no case began and the two cannot be told apart) and the nodes, by class, name and script. suites lists only the suites with something to say (a case that failed, erred or was skipped, a hook that failed, orphans, or an error undeclared or declared and not printed), and suitesPassed counts the rest. Under classes, the classes each addon declares are counted under addedInAddons and changedInAddons rather than named, and a class the rebuild puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. A suite needs a window when it declares const GDHARNESS_NEEDS_WINDOW: bool = true, or calls scene_runner and declares nothing; a run holding one has a window, at the size project.godot gives its viewport and with no sound, and windowed is true and windowNote names those suites and any saying it runs headless that shared the window. On a machine with no display such a run is headless, windowed is false, and windowNote says so. " + IMPORTED_FIRST,
|
|
16411
16472
|
parameters: {
|
|
16412
16473
|
projectPath: PROJECT_PATH,
|
|
16413
16474
|
path: {
|
|
@@ -16680,7 +16741,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16680
16741
|
{
|
|
16681
16742
|
name: "scene_animation",
|
|
16682
16743
|
title: "Animations",
|
|
16683
|
-
description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. The player and the tree are nodes, so scene_node add puts one in the scene first. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. add_track on a path the animation already has a track of that type for adds the keys to that track, as the editor keys a property it already animates, answering existingTrack true and keysAdded beside keys, the track's count; a key at a time that track already has a key at is refused. A key add_track writes past the animation's end, where it never plays, is written and named under pastLength. remove_track takes out every track of the type, nodePath and property given, with its keys; a method track given a method loses only the keys calling it, and goes once none is left. read_states gives each state that plays an animation animationFound, and animationMissing saying why when the player lacks it. remove clears the player's autoplay when it named the animation and names under statesPlayingNothing the states that played it, which stay in their trees playing nothing until remove_state takes them out. Every state machine has Start and End, which cannot be added or removed; a transition from Start sets the state it begins in. read and read_states need no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without running the scene's scripts. The ops that write go through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. An editor of the project that is running and has not reached this server is waited for rather than written around. " + ALSO_SAVED,
|
|
16744
|
+
description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. The player and the tree are nodes, so scene_node add puts one in the scene first, and the animation a player plays on load is its autoplay property, which scene_node set sets. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. add_track on a path the animation already has a track of that type for adds the keys to that track, as the editor keys a property it already animates, answering existingTrack true and keysAdded beside keys, the track's count; a key at a time that track already has a key at is refused unless replaceKeys is true, which is how one key's value is changed. A key add_track writes past the animation's end, where it never plays, is written and named under pastLength. remove_track takes out every track of the type, nodePath and property given, with its keys; a method track given a method loses only the keys calling it, and goes once none is left. read_states gives each state that plays an animation animationFound, and animationMissing saying why when the player lacks it. remove clears the player's autoplay when it named the animation and names under statesPlayingNothing the states that played it, which stay in their trees playing nothing until remove_state takes them out. Every state machine has Start and End, which cannot be added or removed; a transition from Start sets the state it begins in. read and read_states need no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without running the scene's scripts. The ops that write go through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. An editor of the project that is running and has not reached this server is waited for rather than written around. " + ALSO_SAVED,
|
|
16684
16745
|
parameters: {
|
|
16685
16746
|
projectPath: PROJECT_PATH,
|
|
16686
16747
|
scenePath: SCENE_PATH,
|
|
@@ -16716,6 +16777,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
16716
16777
|
description: "create and set: keyframe snap in seconds, 0 or more. create: default 0.1."
|
|
16717
16778
|
},
|
|
16718
16779
|
track: { ...ANIMATION_TRACK, ops: ["add_track", "remove_track"] },
|
|
16780
|
+
replaceKeys: {
|
|
16781
|
+
type: "boolean",
|
|
16782
|
+
ops: ["add_track"],
|
|
16783
|
+
description: "add_track: true puts a keyframe at a time the track already has a key at in that key's place, and names each key replaced under keysReplaced, its time and the value it held (a method key's method and args); keysAdded then counts only the new times. Default false, which refuses the call and writes nothing."
|
|
16784
|
+
},
|
|
16719
16785
|
animTreePath: {
|
|
16720
16786
|
type: "string",
|
|
16721
16787
|
ops: ["read_states", "add_state", "remove_state", "connect_states", "disconnect_states"],
|
|
@@ -16944,7 +17010,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16944
17010
|
{
|
|
16945
17011
|
name: "resource_edit",
|
|
16946
17012
|
title: "Edit resources",
|
|
16947
|
-
description: 'Resource files: read one, create any ClassDB resource as .tres, change one, write a shader, build a TileSet, or set a Theme colour or font size. A material is a create with resourceType StandardMaterial3D, ShaderMaterial or CanvasItemMaterial. read answers with the properties the file sets, in the form modify takes, with resourceType the engine class and scriptClass the class_name of its script when it declares one, and a Theme lists its items there as "Button/colors/font_color". create and modify answer each property given under properties as the file holds it once saved, in the same form. A create or modify whose properties made the engine change others, as a Gradient given more colours than offsets gives each new point an offset of 0, names each of those under alsoChanged with what it held before and holds now. A property holding a resource takes its res:// path; a file never imported is refused with the reimport that imports it. move takes any file of the project, a script or an image as much as a resource, or a directory, and works from the files, so it needs no editor; one serving the project is brought up to it as after a move in its own FileSystem dock, its open scenes and cached resources named for the new paths and the project settings the move rewrote taken from the file. read needs no editor either: with none serving the project, a headless engine reads the file. The ops that write a resource go through the editor when one serves the project; with none, a headless engine runs the tool the editor would and the answer says so under writtenBy. That engine runs the script of the resource, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before the save, and a resource where that cannot be done is refused and the file left as it was.',
|
|
17013
|
+
description: 'Resource files: read one, create any ClassDB resource as .tres, change one, write a shader, build a TileSet, or set a Theme colour or font size. A material is a create with resourceType StandardMaterial3D, ShaderMaterial or CanvasItemMaterial. read answers with the properties the file sets, in the form modify takes, with resourceType the engine class and scriptClass the class_name of its script when it declares one, and a Theme lists its items there as "Button/colors/font_color". A Curve, Curve2D or Curve3D reads as the inspector shows it, point_count and point_0/position and the rest, rather than the _data list the file stores, and so does one built into a resource or a scene. create and modify answer each property given under properties as the file holds it once saved, in the same form. A create or modify whose properties made the engine change others, as a Gradient given more colours than offsets gives each new point an offset of 0, names each of those under alsoChanged with what it held before and holds now. A property holding a resource takes its res:// path; a file never imported is refused with the reimport that imports it. move takes any file of the project, a script or an image as much as a resource, or a directory, and works from the files, so it needs no editor; one serving the project is brought up to it as after a move in its own FileSystem dock, its open scenes and cached resources named for the new paths and the project settings the move rewrote taken from the file. read needs no editor either: with none serving the project, a headless engine reads the file. The ops that write a resource go through the editor when one serves the project; with none, a headless engine runs the tool the editor would and the answer says so under writtenBy. That engine runs the script of the resource, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before the save, and a resource where that cannot be done is refused and the file left as it was.',
|
|
16948
17014
|
parameters: {
|
|
16949
17015
|
projectPath: PROJECT_PATH,
|
|
16950
17016
|
resourcePath: {
|
|
@@ -52462,7 +52528,8 @@ ${platform}`;
|
|
|
52462
52528
|
path: suite.path,
|
|
52463
52529
|
name: report.suites.find((entry) => entry.path === suite.path)?.name ?? suite.path,
|
|
52464
52530
|
orphans: suite.orphans,
|
|
52465
|
-
|
|
52531
|
+
...suite.left.length > 0 ? { left: suite.left.map(({ during, count, nodes }) => ({ case: during, count, nodes })) } : {},
|
|
52532
|
+
message: suite.left.length > 0 ? suite.left.map(orphansLeftSaid).join(" ") : `${suite.orphans === 1 ? "1 node" : `${String(suite.orphans)} nodes`} built during this suite ${suite.orphans === 1 ? "was" : "were"} neither freed nor in the tree when it finished, which gdUnit4 counts as orphans. Free each one, or make it with auto_free(), which frees it once the case is over.`
|
|
52466
52533
|
}));
|
|
52467
52534
|
const judgedErrors = unexpectedErrors(casePrinted, declaredErrors, [...casePrinted.suites.keys()]);
|
|
52468
52535
|
const uncaughtTexts = new Set(uncaught.map((entry) => entry.text));
|
|
@@ -57184,15 +57251,16 @@ var init_server = __esm(() => {
|
|
|
57184
57251
|
// src/cli.ts
|
|
57185
57252
|
init_bridge_announce();
|
|
57186
57253
|
init_engine_launch();
|
|
57254
|
+
init_engine_slots();
|
|
57187
57255
|
import { existsSync as existsSync35 } from "node:fs";
|
|
57188
57256
|
import { dirname as dirname26, join as join44, resolve as resolve14 } from "node:path";
|
|
57189
57257
|
|
|
57190
57258
|
// src/error-mode.ts
|
|
57191
|
-
import
|
|
57259
|
+
import process8 from "node:process";
|
|
57192
57260
|
var NO_FAULT_ERROR_BOX = 2;
|
|
57193
|
-
var cleared =
|
|
57261
|
+
var cleared = process8.platform !== "win32";
|
|
57194
57262
|
async function letWindowsReportCrashes() {
|
|
57195
|
-
if (cleared || !("bun" in
|
|
57263
|
+
if (cleared || !("bun" in process8.versions)) {
|
|
57196
57264
|
return;
|
|
57197
57265
|
}
|
|
57198
57266
|
try {
|
|
@@ -57885,7 +57953,9 @@ async function runSpreadCommand(options, godotPath) {
|
|
|
57885
57953
|
mkdirSync8(shareDirectory, { recursive: true });
|
|
57886
57954
|
const chosen = machineSlots({ directory: options.slotsDirectory, count: options.slotCount });
|
|
57887
57955
|
const slotCount = chosen.count;
|
|
57888
|
-
const slots = new EngineSlots(chosen.directory, slotCount, { pid: process13.pid, started: await ownStart() })
|
|
57956
|
+
const slots = new EngineSlots(chosen.directory, slotCount, { pid: process13.pid, started: await ownStart() }, (line) => {
|
|
57957
|
+
console.log(`[spread] ${line}`);
|
|
57958
|
+
});
|
|
57889
57959
|
const launch = {
|
|
57890
57960
|
godotPath,
|
|
57891
57961
|
projectPath: options.projectPath,
|
|
@@ -58416,9 +58486,8 @@ async function runTestCommand(options, godotPath) {
|
|
|
58416
58486
|
mkdirSync10(runDirectory, { recursive: true });
|
|
58417
58487
|
const logs = new RunLogs(runDirectory, keptRuns === testRuns ? null : join27(keptRuns, runName));
|
|
58418
58488
|
const chosen = machineSlots({ directory: options.slotsDirectory, count: options.slotCount });
|
|
58419
|
-
const slots = new EngineSlots(chosen.directory, chosen.count, {
|
|
58420
|
-
|
|
58421
|
-
started: await ownStart()
|
|
58489
|
+
const slots = new EngineSlots(chosen.directory, chosen.count, { pid: process14.pid, started: await ownStart() }, (line) => {
|
|
58490
|
+
console.log(line);
|
|
58422
58491
|
});
|
|
58423
58492
|
const running = new Set;
|
|
58424
58493
|
const interrupted = () => {
|
|
@@ -59362,6 +59431,11 @@ async function spread() {
|
|
|
59362
59431
|
}
|
|
59363
59432
|
async function main() {
|
|
59364
59433
|
await letWindowsReportCrashes();
|
|
59434
|
+
if (command !== undefined) {
|
|
59435
|
+
sayWaitsTo((line) => {
|
|
59436
|
+
console.error(`gdharness: ${line}`);
|
|
59437
|
+
});
|
|
59438
|
+
}
|
|
59365
59439
|
if (command === "test") {
|
|
59366
59440
|
await test();
|
|
59367
59441
|
return;
|