gdharness 1.3.6 → 1.3.8
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 +1602 -1374
- package/build/godot/addons/gdharness_editor/reading.gd +1 -1
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +12 -10
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +11 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +1 -1
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +45 -1
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +19 -4
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +3 -1
- package/build/godot/addons/gdharness_runtime/runtime_hover.gd +1 -1
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +47 -23
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +28 -2
- package/build/godot/operations/dependencies.gd +61 -10
- package/build/godot/operations/gdscript_analysis.gd +62 -6
- package/build/godot/operations/gdscript_authoring.gd +4 -1
- package/build/godot/operations/godot_operations.gd +2 -0
- package/build/godot/operations/import_pipeline.gd +45 -1
- package/build/godot/operations/project_diagnostics.gd +6 -5
- package/build/godot/operations/reading.gd +1 -1
- package/build/godot/tests/listed.gd +26 -4
- package/build/keeper.js +26 -3
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -8367,9 +8367,487 @@ var init_doc_cache = __esm(() => {
|
|
|
8367
8367
|
CACHE_FILE = /^editor_doc_cache-\d+\.\d+\.res$/;
|
|
8368
8368
|
});
|
|
8369
8369
|
|
|
8370
|
+
// src/project-scan.ts
|
|
8371
|
+
import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync3 } from "node:fs";
|
|
8372
|
+
import { extname, join as join7 } from "node:path";
|
|
8373
|
+
function skipped(name) {
|
|
8374
|
+
return name.startsWith(".") || SKIPPED.has(name);
|
|
8375
|
+
}
|
|
8376
|
+
function steppedOver(directory) {
|
|
8377
|
+
return gdignored(directory) || installedByUs(directory);
|
|
8378
|
+
}
|
|
8379
|
+
function gdignored(directory) {
|
|
8380
|
+
return existsSync6(join7(directory, ".gdignore"));
|
|
8381
|
+
}
|
|
8382
|
+
function installedByUs(directory) {
|
|
8383
|
+
return existsSync6(join7(directory, VERSION_MARKER));
|
|
8384
|
+
}
|
|
8385
|
+
function projectStructure(projectPath) {
|
|
8386
|
+
const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
|
|
8387
|
+
const visit = (directory) => {
|
|
8388
|
+
if (steppedOver(directory)) {
|
|
8389
|
+
return;
|
|
8390
|
+
}
|
|
8391
|
+
for (const entry of readdirSync4(directory, { withFileTypes: true })) {
|
|
8392
|
+
if (skipped(entry.name)) {
|
|
8393
|
+
continue;
|
|
8394
|
+
}
|
|
8395
|
+
if (entry.isDirectory()) {
|
|
8396
|
+
visit(join7(directory, entry.name));
|
|
8397
|
+
} else if (entry.isFile()) {
|
|
8398
|
+
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
8399
|
+
if (SIDECAR_EXTENSIONS.has(extension)) {
|
|
8400
|
+
continue;
|
|
8401
|
+
}
|
|
8402
|
+
if (SCENE_EXTENSIONS.has(extension)) {
|
|
8403
|
+
structure.scenes += 1;
|
|
8404
|
+
} else if (extension === "gd" || extension === "cs") {
|
|
8405
|
+
structure.scripts += 1;
|
|
8406
|
+
} else if (ASSET_EXTENSIONS.has(extension)) {
|
|
8407
|
+
structure.assets += 1;
|
|
8408
|
+
} else {
|
|
8409
|
+
structure.other += 1;
|
|
8410
|
+
}
|
|
8411
|
+
}
|
|
8412
|
+
}
|
|
8413
|
+
};
|
|
8414
|
+
visit(projectPath);
|
|
8415
|
+
return structure;
|
|
8416
|
+
}
|
|
8417
|
+
function projectFiles(projectPath, extensions) {
|
|
8418
|
+
const found = [];
|
|
8419
|
+
const visit = (directory, prefix) => {
|
|
8420
|
+
if (steppedOver(directory)) {
|
|
8421
|
+
return;
|
|
8422
|
+
}
|
|
8423
|
+
for (const entry of readdirSync4(directory, { withFileTypes: true })) {
|
|
8424
|
+
if (skipped(entry.name)) {
|
|
8425
|
+
continue;
|
|
8426
|
+
}
|
|
8427
|
+
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
8428
|
+
if (entry.isDirectory()) {
|
|
8429
|
+
visit(join7(directory, entry.name), spelled);
|
|
8430
|
+
} else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
|
|
8431
|
+
found.push(spelled);
|
|
8432
|
+
}
|
|
8433
|
+
}
|
|
8434
|
+
};
|
|
8435
|
+
visit(projectPath, "");
|
|
8436
|
+
return found.sort();
|
|
8437
|
+
}
|
|
8438
|
+
function scriptsWithoutUid(projectPath) {
|
|
8439
|
+
const found = [];
|
|
8440
|
+
const visit = (directory, prefix) => {
|
|
8441
|
+
if (steppedOver(directory)) {
|
|
8442
|
+
return;
|
|
8443
|
+
}
|
|
8444
|
+
for (const entry of readdirSync4(directory, { withFileTypes: true })) {
|
|
8445
|
+
if (skipped(entry.name)) {
|
|
8446
|
+
continue;
|
|
8447
|
+
}
|
|
8448
|
+
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
8449
|
+
if (entry.isDirectory()) {
|
|
8450
|
+
visit(join7(directory, entry.name), spelled);
|
|
8451
|
+
continue;
|
|
8452
|
+
}
|
|
8453
|
+
if (!entry.isFile()) {
|
|
8454
|
+
continue;
|
|
8455
|
+
}
|
|
8456
|
+
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
8457
|
+
if (CARRIES_A_UID.has(extension) && !existsSync6(join7(directory, `${entry.name}.uid`))) {
|
|
8458
|
+
found.push(spelled);
|
|
8459
|
+
}
|
|
8460
|
+
}
|
|
8461
|
+
};
|
|
8462
|
+
visit(projectPath, "");
|
|
8463
|
+
return found.sort();
|
|
8464
|
+
}
|
|
8465
|
+
function pathsInUidCache(projectPath) {
|
|
8466
|
+
let data;
|
|
8467
|
+
try {
|
|
8468
|
+
data = readFileSync3(join7(projectPath, ".godot", "uid_cache.bin"));
|
|
8469
|
+
} catch {
|
|
8470
|
+
return null;
|
|
8471
|
+
}
|
|
8472
|
+
if (data.length < 4) {
|
|
8473
|
+
return null;
|
|
8474
|
+
}
|
|
8475
|
+
const count = data.readUInt32LE(0);
|
|
8476
|
+
const paths = new Map;
|
|
8477
|
+
let at = 4;
|
|
8478
|
+
for (let entry = 0;entry < count; entry++) {
|
|
8479
|
+
if (at + 12 > data.length) {
|
|
8480
|
+
return null;
|
|
8481
|
+
}
|
|
8482
|
+
const id = data.readBigUInt64LE(at);
|
|
8483
|
+
const length = data.readUInt32LE(at + 8);
|
|
8484
|
+
at += 12;
|
|
8485
|
+
if (at + length > data.length) {
|
|
8486
|
+
return null;
|
|
8487
|
+
}
|
|
8488
|
+
paths.set(id, data.toString("utf8", at, at + length));
|
|
8489
|
+
at += length;
|
|
8490
|
+
}
|
|
8491
|
+
return new Set(paths.values());
|
|
8492
|
+
}
|
|
8493
|
+
function isBinary(bytes) {
|
|
8494
|
+
return bytes.subarray(0, 8000).includes(0);
|
|
8495
|
+
}
|
|
8496
|
+
function projectTexts(projectPath) {
|
|
8497
|
+
const found = [];
|
|
8498
|
+
const visit = (directory, prefix) => {
|
|
8499
|
+
if (steppedOver(directory)) {
|
|
8500
|
+
return;
|
|
8501
|
+
}
|
|
8502
|
+
for (const entry of readdirSync4(directory, { withFileTypes: true })) {
|
|
8503
|
+
if (skipped(entry.name)) {
|
|
8504
|
+
continue;
|
|
8505
|
+
}
|
|
8506
|
+
const absolute = join7(directory, entry.name);
|
|
8507
|
+
const spelled = `${prefix}${entry.name}`;
|
|
8508
|
+
if (entry.isDirectory()) {
|
|
8509
|
+
visit(absolute, `${spelled}/`);
|
|
8510
|
+
continue;
|
|
8511
|
+
}
|
|
8512
|
+
const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
|
|
8513
|
+
if (!entry.isFile() || BINARY_EXTENSIONS.has(extension)) {
|
|
8514
|
+
continue;
|
|
8515
|
+
}
|
|
8516
|
+
const bytes = readFileSync3(absolute);
|
|
8517
|
+
if (!isBinary(bytes)) {
|
|
8518
|
+
found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
|
|
8519
|
+
}
|
|
8520
|
+
}
|
|
8521
|
+
};
|
|
8522
|
+
visit(projectPath, "");
|
|
8523
|
+
return found;
|
|
8524
|
+
}
|
|
8525
|
+
function ignoredTexts(projectPath) {
|
|
8526
|
+
const found = [];
|
|
8527
|
+
const visit = (directory, prefix, ignored) => {
|
|
8528
|
+
if (installedByUs(directory)) {
|
|
8529
|
+
return;
|
|
8530
|
+
}
|
|
8531
|
+
const under = ignored || gdignored(directory);
|
|
8532
|
+
for (const entry of readdirSync4(directory, { withFileTypes: true })) {
|
|
8533
|
+
if (skipped(entry.name)) {
|
|
8534
|
+
continue;
|
|
8535
|
+
}
|
|
8536
|
+
const absolute = join7(directory, entry.name);
|
|
8537
|
+
const spelled = `${prefix}${entry.name}`;
|
|
8538
|
+
if (entry.isDirectory()) {
|
|
8539
|
+
visit(absolute, `${spelled}/`, under);
|
|
8540
|
+
continue;
|
|
8541
|
+
}
|
|
8542
|
+
const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
|
|
8543
|
+
if (!under || !entry.isFile() || BINARY_EXTENSIONS.has(extension) || entry.name === ".gdignore") {
|
|
8544
|
+
continue;
|
|
8545
|
+
}
|
|
8546
|
+
const bytes = readFileSync3(absolute);
|
|
8547
|
+
if (!isBinary(bytes)) {
|
|
8548
|
+
found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
|
|
8549
|
+
}
|
|
8550
|
+
}
|
|
8551
|
+
};
|
|
8552
|
+
visit(projectPath, "", false);
|
|
8553
|
+
return found;
|
|
8554
|
+
}
|
|
8555
|
+
function plainMatch(line, needle, caseSensitive) {
|
|
8556
|
+
if (caseSensitive) {
|
|
8557
|
+
return line.includes(needle) ? [needle] : null;
|
|
8558
|
+
}
|
|
8559
|
+
const lowered = line.toLowerCase();
|
|
8560
|
+
const at = lowered.indexOf(needle);
|
|
8561
|
+
if (at < 0) {
|
|
8562
|
+
return null;
|
|
8563
|
+
}
|
|
8564
|
+
return [lowered.length === line.length ? line.slice(at, at + needle.length) : needle];
|
|
8565
|
+
}
|
|
8566
|
+
function compiledQuery(query, caseSensitive) {
|
|
8567
|
+
try {
|
|
8568
|
+
return new RegExp(query, caseSensitive ? "" : "i");
|
|
8569
|
+
} catch (error) {
|
|
8570
|
+
const why = errorMessage(error).split(": ").at(-1) ?? "";
|
|
8571
|
+
throw new Error(`'${query}' is not a regular expression: ${why.charAt(0).toLowerCase()}${why.slice(1)}. With regex true, a character such as ( ) [ ] { } . * + ? | ^ $ or \\ stands for itself only after a backslash, as in \\(; or leave regex out to search for the text as written.`);
|
|
8572
|
+
}
|
|
8573
|
+
}
|
|
8574
|
+
function searchProject(projectPath, options) {
|
|
8575
|
+
const extensions = options.fileTypes === undefined ? null : new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
|
|
8576
|
+
const result = {
|
|
8577
|
+
query: options.query,
|
|
8578
|
+
results: [],
|
|
8579
|
+
summary: { files_searched: 0, files_with_matches: 0, total_matches: 0, truncated: false }
|
|
8580
|
+
};
|
|
8581
|
+
const regex = options.regex ? compiledQuery(options.query, options.caseSensitive) : null;
|
|
8582
|
+
const needle = options.caseSensitive ? options.query : options.query.toLowerCase();
|
|
8583
|
+
const full = () => result.summary.truncated;
|
|
8584
|
+
const notSearched = [];
|
|
8585
|
+
const visit = (directory) => {
|
|
8586
|
+
if (steppedOver(directory)) {
|
|
8587
|
+
if (gdignored(directory)) {
|
|
8588
|
+
notSearched.push(`res://${directory.slice(projectPath.length + 1).replace(/\\/g, "/")}/`);
|
|
8589
|
+
}
|
|
8590
|
+
return;
|
|
8591
|
+
}
|
|
8592
|
+
for (const entry of readdirSync4(directory, { withFileTypes: true })) {
|
|
8593
|
+
if (full()) {
|
|
8594
|
+
return;
|
|
8595
|
+
}
|
|
8596
|
+
if (skipped(entry.name)) {
|
|
8597
|
+
continue;
|
|
8598
|
+
}
|
|
8599
|
+
const entryPath = join7(directory, entry.name);
|
|
8600
|
+
if (entry.isDirectory()) {
|
|
8601
|
+
visit(entryPath);
|
|
8602
|
+
continue;
|
|
8603
|
+
}
|
|
8604
|
+
const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
|
|
8605
|
+
if (!entry.isFile() || extensions !== null && !extensions.has(extension)) {
|
|
8606
|
+
continue;
|
|
8607
|
+
}
|
|
8608
|
+
const bytes = readFileSync3(entryPath);
|
|
8609
|
+
if (extensions === null && isBinary(bytes)) {
|
|
8610
|
+
continue;
|
|
8611
|
+
}
|
|
8612
|
+
result.summary.files_searched += 1;
|
|
8613
|
+
const matches = [];
|
|
8614
|
+
for (const [index, line] of bytes.toString("utf8").split(/\r?\n/).entries()) {
|
|
8615
|
+
const found = regex ? regex.exec(line) : plainMatch(line, needle, options.caseSensitive);
|
|
8616
|
+
if (found === null) {
|
|
8617
|
+
continue;
|
|
8618
|
+
}
|
|
8619
|
+
if (result.summary.total_matches >= options.maxResults) {
|
|
8620
|
+
result.summary.truncated = true;
|
|
8621
|
+
break;
|
|
8622
|
+
}
|
|
8623
|
+
matches.push({ line: index + 1, content: line.trim(), match: found[0] });
|
|
8624
|
+
result.summary.total_matches += 1;
|
|
8625
|
+
}
|
|
8626
|
+
if (matches.length > 0) {
|
|
8627
|
+
const relativePath = entryPath.slice(projectPath.length + 1).replace(/\\/g, "/");
|
|
8628
|
+
result.results.push({ file: `res://${relativePath}`, matches });
|
|
8629
|
+
result.summary.files_with_matches += 1;
|
|
8630
|
+
}
|
|
8631
|
+
}
|
|
8632
|
+
};
|
|
8633
|
+
visit(projectPath);
|
|
8634
|
+
return notSearched.length === 0 ? result : { ...result, not_searched: notSearched };
|
|
8635
|
+
}
|
|
8636
|
+
var SKIPPED, VERSION_MARKER = ".gdharness-version", SCENE_EXTENSIONS, SIDECAR_EXTENSIONS, ASSET_EXTENSIONS, CARRIES_A_UID, BINARY_EXTENSIONS;
|
|
8637
|
+
var init_project_scan = __esm(() => {
|
|
8638
|
+
init_errors();
|
|
8639
|
+
SKIPPED = new Set(["node_modules"]);
|
|
8640
|
+
SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
|
|
8641
|
+
SIDECAR_EXTENSIONS = new Set(["import", "uid"]);
|
|
8642
|
+
ASSET_EXTENSIONS = new Set([
|
|
8643
|
+
"png",
|
|
8644
|
+
"jpg",
|
|
8645
|
+
"jpeg",
|
|
8646
|
+
"webp",
|
|
8647
|
+
"svg",
|
|
8648
|
+
"bmp",
|
|
8649
|
+
"tga",
|
|
8650
|
+
"exr",
|
|
8651
|
+
"hdr",
|
|
8652
|
+
"ktx",
|
|
8653
|
+
"dds",
|
|
8654
|
+
"ttf",
|
|
8655
|
+
"otf",
|
|
8656
|
+
"woff",
|
|
8657
|
+
"woff2",
|
|
8658
|
+
"fnt",
|
|
8659
|
+
"wav",
|
|
8660
|
+
"mp3",
|
|
8661
|
+
"ogg",
|
|
8662
|
+
"glb",
|
|
8663
|
+
"gltf",
|
|
8664
|
+
"obj",
|
|
8665
|
+
"fbx",
|
|
8666
|
+
"blend",
|
|
8667
|
+
"dae",
|
|
8668
|
+
"ogv",
|
|
8669
|
+
"gdshader",
|
|
8670
|
+
"shader"
|
|
8671
|
+
]);
|
|
8672
|
+
CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
|
|
8673
|
+
BINARY_EXTENSIONS = new Set([
|
|
8674
|
+
...[...ASSET_EXTENSIONS].filter((extension) => !["svg", "gltf", "gdshader", "shader", "fnt"].includes(extension)),
|
|
8675
|
+
"scn",
|
|
8676
|
+
"res",
|
|
8677
|
+
"zip",
|
|
8678
|
+
"pck",
|
|
8679
|
+
"exe",
|
|
8680
|
+
"dll",
|
|
8681
|
+
"so",
|
|
8682
|
+
"dylib"
|
|
8683
|
+
]);
|
|
8684
|
+
});
|
|
8685
|
+
|
|
8686
|
+
// src/import-freshness.ts
|
|
8687
|
+
import { createHash } from "node:crypto";
|
|
8688
|
+
import { existsSync as existsSync7, readdirSync as readdirSync5, readFileSync as readFileSync4, statSync as statSync5 } from "node:fs";
|
|
8689
|
+
import { join as join8 } from "node:path";
|
|
8690
|
+
function md5Of(file) {
|
|
8691
|
+
let stats;
|
|
8692
|
+
try {
|
|
8693
|
+
stats = statSync5(file);
|
|
8694
|
+
} catch {
|
|
8695
|
+
return null;
|
|
8696
|
+
}
|
|
8697
|
+
const known = hashes.get(file);
|
|
8698
|
+
if (known !== undefined && known.size === stats.size && known.mtimeMs === stats.mtimeMs) {
|
|
8699
|
+
return known.md5;
|
|
8700
|
+
}
|
|
8701
|
+
let md5;
|
|
8702
|
+
try {
|
|
8703
|
+
md5 = createHash("md5").update(readFileSync4(file)).digest("hex");
|
|
8704
|
+
} catch {
|
|
8705
|
+
return null;
|
|
8706
|
+
}
|
|
8707
|
+
hashes.set(file, { size: stats.size, mtimeMs: stats.mtimeMs, md5 });
|
|
8708
|
+
return md5;
|
|
8709
|
+
}
|
|
8710
|
+
function sectionOf(text, section) {
|
|
8711
|
+
const keys = new Map;
|
|
8712
|
+
let inside = false;
|
|
8713
|
+
for (const line of text.split(/\r?\n/)) {
|
|
8714
|
+
const header = /^\[([^\]]+)\]\s*$/.exec(line);
|
|
8715
|
+
if (header !== null) {
|
|
8716
|
+
inside = header[1] === section;
|
|
8717
|
+
continue;
|
|
8718
|
+
}
|
|
8719
|
+
const pair = inside ? /^([A-Za-z0-9_./]+)=(.*)$/.exec(line) : null;
|
|
8720
|
+
if (pair?.[1] !== undefined && pair[2] !== undefined) {
|
|
8721
|
+
keys.set(pair[1], pair[2]);
|
|
8722
|
+
}
|
|
8723
|
+
}
|
|
8724
|
+
return keys;
|
|
8725
|
+
}
|
|
8726
|
+
function unquoted(value) {
|
|
8727
|
+
return value === undefined ? "" : value.replace(/^"(.*)"$/, "$1");
|
|
8728
|
+
}
|
|
8729
|
+
function listed(value) {
|
|
8730
|
+
return [...(value ?? "").matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((match) => match[1] ?? "");
|
|
8731
|
+
}
|
|
8732
|
+
function onDisk(projectPath, resPath) {
|
|
8733
|
+
return join8(projectPath, resPath.slice("res://".length));
|
|
8734
|
+
}
|
|
8735
|
+
function behind(projectPath, resPath, sidecar) {
|
|
8736
|
+
const remap = sectionOf(sidecar, "remap");
|
|
8737
|
+
const importer = unquoted(remap.get("importer"));
|
|
8738
|
+
if (importer === "keep" || importer === "skip") {
|
|
8739
|
+
return null;
|
|
8740
|
+
}
|
|
8741
|
+
const outputs = listed(sectionOf(sidecar, "deps").get("dest_files"));
|
|
8742
|
+
if (outputs.some((output) => !existsSync7(onDisk(projectPath, output)))) {
|
|
8743
|
+
return "its imported output is not on disk";
|
|
8744
|
+
}
|
|
8745
|
+
const name = resPath.slice(resPath.lastIndexOf("/") + 1);
|
|
8746
|
+
const record = join8(projectPath, ".godot", "imported", `${name}-${createHash("md5").update(resPath, "utf8").digest("hex")}.md5`);
|
|
8747
|
+
let recorded = "";
|
|
8748
|
+
try {
|
|
8749
|
+
recorded = /^source_md5="([0-9a-f]*)"/m.exec(readFileSync4(record, "utf8"))?.[1] ?? "";
|
|
8750
|
+
} catch {
|
|
8751
|
+
recorded = "";
|
|
8752
|
+
}
|
|
8753
|
+
if (recorded === "") {
|
|
8754
|
+
return "its import recorded no hash of its source";
|
|
8755
|
+
}
|
|
8756
|
+
const current = md5Of(onDisk(projectPath, resPath));
|
|
8757
|
+
return current === null || current === recorded ? null : "the source changed after it was imported";
|
|
8758
|
+
}
|
|
8759
|
+
function staleImports(projectPath) {
|
|
8760
|
+
const stale = [];
|
|
8761
|
+
const visit = (directory, resDirectory) => {
|
|
8762
|
+
if (existsSync7(join8(directory, ".gdignore")) || existsSync7(join8(directory, VERSION_MARKER))) {
|
|
8763
|
+
return;
|
|
8764
|
+
}
|
|
8765
|
+
let entries;
|
|
8766
|
+
try {
|
|
8767
|
+
entries = readdirSync5(directory, { withFileTypes: true });
|
|
8768
|
+
} catch {
|
|
8769
|
+
return;
|
|
8770
|
+
}
|
|
8771
|
+
const names = new Set(entries.map((entry) => entry.name));
|
|
8772
|
+
for (const entry of entries) {
|
|
8773
|
+
if (entry.name.startsWith(".")) {
|
|
8774
|
+
continue;
|
|
8775
|
+
}
|
|
8776
|
+
const resPath = `${resDirectory}${entry.name}`;
|
|
8777
|
+
if (entry.isDirectory()) {
|
|
8778
|
+
visit(join8(directory, entry.name), `${resPath}/`);
|
|
8779
|
+
continue;
|
|
8780
|
+
}
|
|
8781
|
+
if (!entry.isFile()) {
|
|
8782
|
+
continue;
|
|
8783
|
+
}
|
|
8784
|
+
if (entry.name.endsWith(".import")) {
|
|
8785
|
+
const source = resPath.slice(0, -".import".length);
|
|
8786
|
+
if (!names.has(entry.name.slice(0, -".import".length))) {
|
|
8787
|
+
continue;
|
|
8788
|
+
}
|
|
8789
|
+
let sidecar;
|
|
8790
|
+
try {
|
|
8791
|
+
sidecar = readFileSync4(join8(directory, entry.name), "utf8");
|
|
8792
|
+
} catch {
|
|
8793
|
+
continue;
|
|
8794
|
+
}
|
|
8795
|
+
const reason = behind(projectPath, source, sidecar);
|
|
8796
|
+
if (reason !== null) {
|
|
8797
|
+
stale.push({ path: source, reason });
|
|
8798
|
+
}
|
|
8799
|
+
continue;
|
|
8800
|
+
}
|
|
8801
|
+
const extension = entry.name.slice(entry.name.lastIndexOf(".") + 1).toLowerCase();
|
|
8802
|
+
if (IMPORTABLE.has(extension) && !names.has(`${entry.name}.import`)) {
|
|
8803
|
+
stale.push({ path: resPath, reason: "it has not been imported" });
|
|
8804
|
+
}
|
|
8805
|
+
}
|
|
8806
|
+
};
|
|
8807
|
+
visit(projectPath, "res://");
|
|
8808
|
+
return stale.sort((one, other) => one.path.localeCompare(other.path));
|
|
8809
|
+
}
|
|
8810
|
+
var IMPORTABLE_EXTENSIONS, IMPORTABLE, hashes;
|
|
8811
|
+
var init_import_freshness = __esm(() => {
|
|
8812
|
+
init_project_scan();
|
|
8813
|
+
IMPORTABLE_EXTENSIONS = [
|
|
8814
|
+
"png",
|
|
8815
|
+
"jpg",
|
|
8816
|
+
"jpeg",
|
|
8817
|
+
"webp",
|
|
8818
|
+
"svg",
|
|
8819
|
+
"bmp",
|
|
8820
|
+
"tga",
|
|
8821
|
+
"exr",
|
|
8822
|
+
"hdr",
|
|
8823
|
+
"dds",
|
|
8824
|
+
"ktx",
|
|
8825
|
+
"wav",
|
|
8826
|
+
"mp3",
|
|
8827
|
+
"ogg",
|
|
8828
|
+
"ttf",
|
|
8829
|
+
"otf",
|
|
8830
|
+
"ttc",
|
|
8831
|
+
"otc",
|
|
8832
|
+
"woff",
|
|
8833
|
+
"woff2",
|
|
8834
|
+
"pfb",
|
|
8835
|
+
"pfm",
|
|
8836
|
+
"fnt",
|
|
8837
|
+
"font",
|
|
8838
|
+
"glb",
|
|
8839
|
+
"gltf",
|
|
8840
|
+
"fbx",
|
|
8841
|
+
"obj",
|
|
8842
|
+
"dae"
|
|
8843
|
+
];
|
|
8844
|
+
IMPORTABLE = new Set(IMPORTABLE_EXTENSIONS);
|
|
8845
|
+
hashes = new Map;
|
|
8846
|
+
});
|
|
8847
|
+
|
|
8370
8848
|
// src/library-copies.ts
|
|
8371
|
-
import { existsSync as
|
|
8372
|
-
import { basename as basename5, dirname as dirname6, isAbsolute as isAbsolute3, join as
|
|
8849
|
+
import { existsSync as existsSync8, readdirSync as readdirSync6, readFileSync as readFileSync5, renameSync, unlinkSync } from "node:fs";
|
|
8850
|
+
import { basename as basename5, dirname as dirname6, isAbsolute as isAbsolute3, join as join9 } from "node:path";
|
|
8373
8851
|
function freeLibraryCopies(projectPath, platform = process.platform) {
|
|
8374
8852
|
const setAside = [];
|
|
8375
8853
|
const swept = [];
|
|
@@ -8379,24 +8857,24 @@ function freeLibraryCopies(projectPath, platform = process.platform) {
|
|
|
8379
8857
|
for (const library of windowsLibraries(projectPath)) {
|
|
8380
8858
|
const folder = dirname6(library);
|
|
8381
8859
|
const copy = `~${basename5(library)}`;
|
|
8382
|
-
if (
|
|
8383
|
-
const aside =
|
|
8860
|
+
if (existsSync8(join9(folder, copy))) {
|
|
8861
|
+
const aside = join9(folder, `${copy}.aside-${process.pid}-${Date.now()}`);
|
|
8384
8862
|
try {
|
|
8385
|
-
renameSync(
|
|
8863
|
+
renameSync(join9(folder, copy), aside);
|
|
8386
8864
|
setAside.push(aside);
|
|
8387
8865
|
} catch {}
|
|
8388
8866
|
}
|
|
8389
8867
|
let names;
|
|
8390
8868
|
try {
|
|
8391
|
-
names =
|
|
8869
|
+
names = readdirSync6(folder);
|
|
8392
8870
|
} catch {
|
|
8393
8871
|
continue;
|
|
8394
8872
|
}
|
|
8395
8873
|
for (const name of names) {
|
|
8396
8874
|
if (name.length > copy.length && name.toLowerCase().startsWith(copy.toLowerCase())) {
|
|
8397
8875
|
try {
|
|
8398
|
-
unlinkSync(
|
|
8399
|
-
swept.push(
|
|
8876
|
+
unlinkSync(join9(folder, name));
|
|
8877
|
+
swept.push(join9(folder, name));
|
|
8400
8878
|
} catch {}
|
|
8401
8879
|
}
|
|
8402
8880
|
}
|
|
@@ -8404,7 +8882,7 @@ function freeLibraryCopies(projectPath, platform = process.platform) {
|
|
|
8404
8882
|
return { setAside, swept };
|
|
8405
8883
|
}
|
|
8406
8884
|
function windowsLibraries(projectPath) {
|
|
8407
|
-
const listed = readOrNull(
|
|
8885
|
+
const listed = readOrNull(join9(projectPath, ".godot", "extension_list.cfg"));
|
|
8408
8886
|
if (listed === null) {
|
|
8409
8887
|
return [];
|
|
8410
8888
|
}
|
|
@@ -8436,13 +8914,13 @@ function resolveResource(projectPath, relativeTo, path) {
|
|
|
8436
8914
|
return null;
|
|
8437
8915
|
}
|
|
8438
8916
|
if (path.startsWith("res://")) {
|
|
8439
|
-
return
|
|
8917
|
+
return join9(projectPath, path.slice("res://".length));
|
|
8440
8918
|
}
|
|
8441
|
-
return isAbsolute3(path) ? path :
|
|
8919
|
+
return isAbsolute3(path) ? path : join9(relativeTo, path);
|
|
8442
8920
|
}
|
|
8443
8921
|
function readOrNull(path) {
|
|
8444
8922
|
try {
|
|
8445
|
-
return
|
|
8923
|
+
return readFileSync5(path, "utf8");
|
|
8446
8924
|
} catch {
|
|
8447
8925
|
return null;
|
|
8448
8926
|
}
|
|
@@ -8450,23 +8928,23 @@ function readOrNull(path) {
|
|
|
8450
8928
|
var init_library_copies = () => {};
|
|
8451
8929
|
|
|
8452
8930
|
// src/reimport.ts
|
|
8453
|
-
import { createHash } from "node:crypto";
|
|
8454
|
-
import { existsSync as
|
|
8455
|
-
import { basename as basename6, join as
|
|
8931
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
8932
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6, rmSync as rmSync3, statSync as statSync6, utimesSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
8933
|
+
import { basename as basename6, join as join10 } from "node:path";
|
|
8456
8934
|
function forceNextImport(projectPath, resourcePath) {
|
|
8457
8935
|
const relative = resourcePath.replace(/^res:\/\//, "");
|
|
8458
|
-
const sidecar =
|
|
8459
|
-
if (!
|
|
8936
|
+
const sidecar = join10(projectPath, `${relative}.import`);
|
|
8937
|
+
if (!existsSync9(sidecar)) {
|
|
8460
8938
|
return;
|
|
8461
8939
|
}
|
|
8462
|
-
const { atime, mtimeMs } =
|
|
8463
|
-
writeFileSync3(sidecar,
|
|
8940
|
+
const { atime, mtimeMs } = statSync6(sidecar);
|
|
8941
|
+
writeFileSync3(sidecar, readFileSync6(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
|
|
8464
8942
|
utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
|
|
8465
8943
|
rmSync3(importRecord(projectPath, resourcePath), { force: true });
|
|
8466
8944
|
}
|
|
8467
8945
|
function importRecord(projectPath, resourcePath) {
|
|
8468
|
-
const digest =
|
|
8469
|
-
return
|
|
8946
|
+
const digest = createHash2("md5").update(resourcePath).digest("hex");
|
|
8947
|
+
return join10(projectPath, ".godot", "imported", `${basename6(resourcePath)}-${digest}.md5`);
|
|
8470
8948
|
}
|
|
8471
8949
|
var init_reimport = () => {};
|
|
8472
8950
|
|
|
@@ -8496,17 +8974,17 @@ function alive(pid) {
|
|
|
8496
8974
|
var init_alive = () => {};
|
|
8497
8975
|
|
|
8498
8976
|
// src/scratch.ts
|
|
8499
|
-
import { existsSync as
|
|
8977
|
+
import { existsSync as existsSync10, mkdtempSync, readdirSync as readdirSync7, rmSync as rmSync4 } from "node:fs";
|
|
8500
8978
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
8501
|
-
import { join as
|
|
8979
|
+
import { join as join11 } from "node:path";
|
|
8502
8980
|
import process8 from "node:process";
|
|
8503
8981
|
function scratchDirectory(kind) {
|
|
8504
|
-
return mkdtempSync(
|
|
8982
|
+
return mkdtempSync(join11(tmpdir2(), `gdharness-${kind}-${process8.pid}-`));
|
|
8505
8983
|
}
|
|
8506
8984
|
function sweepAbandonedScratch(directory = tmpdir2()) {
|
|
8507
8985
|
let entries;
|
|
8508
8986
|
try {
|
|
8509
|
-
entries =
|
|
8987
|
+
entries = readdirSync7(directory);
|
|
8510
8988
|
} catch {
|
|
8511
8989
|
return;
|
|
8512
8990
|
}
|
|
@@ -8517,7 +8995,7 @@ function sweepAbandonedScratch(directory = tmpdir2()) {
|
|
|
8517
8995
|
continue;
|
|
8518
8996
|
}
|
|
8519
8997
|
try {
|
|
8520
|
-
remove(
|
|
8998
|
+
remove(join11(directory, entry));
|
|
8521
8999
|
} catch {}
|
|
8522
9000
|
}
|
|
8523
9001
|
}
|
|
@@ -8555,7 +9033,7 @@ function tryAgain(path, attempt) {
|
|
|
8555
9033
|
lastTryOnExit();
|
|
8556
9034
|
}
|
|
8557
9035
|
const after = AGAIN_AFTER_MS[attempt];
|
|
8558
|
-
if (after === undefined || !
|
|
9036
|
+
if (after === undefined || !existsSync10(path)) {
|
|
8559
9037
|
waiting.delete(path);
|
|
8560
9038
|
if (after === undefined) {
|
|
8561
9039
|
atExit.add(path);
|
|
@@ -8744,6 +9222,29 @@ function ignoredYetRan(report, ignored) {
|
|
|
8744
9222
|
}
|
|
8745
9223
|
return found;
|
|
8746
9224
|
}
|
|
9225
|
+
function ignoredLeftOut(report, ignored, declared) {
|
|
9226
|
+
const nameOf = (path) => (path.split("/").at(-1) ?? path).replace(/\.gd$/, "");
|
|
9227
|
+
const leftOut = new Set;
|
|
9228
|
+
const unmatched = [];
|
|
9229
|
+
for (const entry of ignored) {
|
|
9230
|
+
const bare = entry.replace("res://", "");
|
|
9231
|
+
const at = bare.lastIndexOf(":");
|
|
9232
|
+
const suiteNamed = at === -1 ? bare : bare.slice(0, at);
|
|
9233
|
+
const testNamed = at === -1 ? null : bare.slice(at + 1);
|
|
9234
|
+
const named = declared.filter((suite) => nameOf(suite.path) === suiteNamed || suite.path.includes(suiteNamed)).flatMap((suite) => suite.cases.filter((one) => testNamed === null || one === testNamed).map((test) => ({ suite, test })));
|
|
9235
|
+
if (named.length === 0) {
|
|
9236
|
+
unmatched.push(entry);
|
|
9237
|
+
continue;
|
|
9238
|
+
}
|
|
9239
|
+
for (const { suite, test } of named) {
|
|
9240
|
+
const reported = report.suites.filter((ran) => ran.path === suite.path || ran.path === null && ran.name === nameOf(suite.path)).some((ran) => ran.cases.some((one) => one.name === test || new RegExp(`^${test}\\W`).test(one.name)));
|
|
9241
|
+
if (!reported) {
|
|
9242
|
+
leftOut.add(`${nameOf(suite.path)}:${test}`);
|
|
9243
|
+
}
|
|
9244
|
+
}
|
|
9245
|
+
}
|
|
9246
|
+
return { leftOut: [...leftOut].sort(), unmatched };
|
|
9247
|
+
}
|
|
8747
9248
|
function caseOf(suite, element) {
|
|
8748
9249
|
const of = (kind) => element.children.filter((child) => child.name === kind).map((child) => {
|
|
8749
9250
|
const detail = child.text.trim();
|
|
@@ -8776,6 +9277,7 @@ function parseJUnit(xml) {
|
|
|
8776
9277
|
throw new MalformedReportError("No <testsuites> or <testsuite> in the report");
|
|
8777
9278
|
}
|
|
8778
9279
|
const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
|
|
9280
|
+
const where = (suite) => suite.path ?? suite.name;
|
|
8779
9281
|
const suites = suiteElements.map((suite) => {
|
|
8780
9282
|
const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
|
|
8781
9283
|
const failures = cases.filter(failedAnAssertion).length;
|
|
@@ -8795,6 +9297,7 @@ function parseJUnit(xml) {
|
|
|
8795
9297
|
cases
|
|
8796
9298
|
};
|
|
8797
9299
|
});
|
|
9300
|
+
suites.sort((a, b) => where(a) < where(b) ? -1 : where(a) > where(b) ? 1 : 0);
|
|
8798
9301
|
const all = suites.flatMap((suite) => suite.cases);
|
|
8799
9302
|
return {
|
|
8800
9303
|
tests: all.length,
|
|
@@ -9102,7 +9605,7 @@ var init_junit = __esm(() => {
|
|
|
9102
9605
|
});
|
|
9103
9606
|
|
|
9104
9607
|
// src/script-origins.ts
|
|
9105
|
-
import { join as
|
|
9608
|
+
import { join as join12 } from "node:path";
|
|
9106
9609
|
function unresolvedClassIn(message) {
|
|
9107
9610
|
return UNRESOLVED.exec(message)?.[1] ?? null;
|
|
9108
9611
|
}
|
|
@@ -9164,7 +9667,7 @@ function checkWithEngine(godotPath, projectPath) {
|
|
|
9164
9667
|
const ran = await runEngine(godotPath, [
|
|
9165
9668
|
"--headless",
|
|
9166
9669
|
"--log-file",
|
|
9167
|
-
|
|
9670
|
+
join12(logDir, "engine.log"),
|
|
9168
9671
|
"--path",
|
|
9169
9672
|
projectPath,
|
|
9170
9673
|
"--check-only",
|
|
@@ -9210,8 +9713,8 @@ var init_script_origins = __esm(() => {
|
|
|
9210
9713
|
});
|
|
9211
9714
|
|
|
9212
9715
|
// src/headless.ts
|
|
9213
|
-
import { existsSync as
|
|
9214
|
-
import { dirname as dirname7, join as
|
|
9716
|
+
import { existsSync as existsSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
9717
|
+
import { dirname as dirname7, join as join13 } from "node:path";
|
|
9215
9718
|
function snakeCased(params) {
|
|
9216
9719
|
const result = emptyRecord();
|
|
9217
9720
|
for (const [key, value] of Object.entries(params)) {
|
|
@@ -9223,8 +9726,10 @@ function snakeCased(params) {
|
|
|
9223
9726
|
function engineExtras(outcome) {
|
|
9224
9727
|
const after = outcome.afterAnswerMessages ?? [];
|
|
9225
9728
|
const before = outcome.beforeOperationMessages ?? [];
|
|
9729
|
+
const notImported = notImportedNote(outcome.messages);
|
|
9226
9730
|
return {
|
|
9227
9731
|
...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
|
|
9732
|
+
...notImported === undefined ? {} : { engine_messages_note: notImported },
|
|
9228
9733
|
...before.length > 0 ? {
|
|
9229
9734
|
engine_messages_before_operation: before,
|
|
9230
9735
|
engine_messages_before_operation_note: withNotCompiled(BEFORE_OPERATION_NOTE, before)
|
|
@@ -9243,6 +9748,20 @@ function withNotCompiled(note, entries) {
|
|
|
9243
9748
|
const path = entries.map(notCompiledIn).find((found) => found !== undefined);
|
|
9244
9749
|
return path === undefined ? note : `${note} ${path} did not compile, so every run of this project starts its autoloads without it until it is fixed; its errors are among these.`;
|
|
9245
9750
|
}
|
|
9751
|
+
function notImportedNote(entries) {
|
|
9752
|
+
const paths = [
|
|
9753
|
+
...new Set(entries.flatMap((entry) => {
|
|
9754
|
+
const path = /^No loader found for resource: (res:\/\/\S+)/.exec(entry.text)?.[1];
|
|
9755
|
+
const extension = path?.split(".").pop()?.toLowerCase() ?? "";
|
|
9756
|
+
return path !== undefined && IMPORTABLE_EXTENSIONS.includes(extension) ? [path] : [];
|
|
9757
|
+
}))
|
|
9758
|
+
];
|
|
9759
|
+
if (paths.length === 0) {
|
|
9760
|
+
return;
|
|
9761
|
+
}
|
|
9762
|
+
const one = paths.length === 1;
|
|
9763
|
+
return `${paths.join(", ")} ${one ? "has" : "have"} no import the engine can load, which is how a file it imports stands before it is first imported, so what uses ${one ? "it" : "them"} reads ${one ? "it" : "them"} as missing although ${one ? "it is" : "they are"} on disk: project_import reimport imports ${one ? "it" : "them"}.`;
|
|
9764
|
+
}
|
|
9246
9765
|
function withoutAsides(entries) {
|
|
9247
9766
|
const kept = [];
|
|
9248
9767
|
let aside = false;
|
|
@@ -9349,7 +9868,7 @@ async function importOnce(godotPath, projectPath) {
|
|
|
9349
9868
|
ran = await runEngine(godotPath, [
|
|
9350
9869
|
"--headless",
|
|
9351
9870
|
"--log-file",
|
|
9352
|
-
|
|
9871
|
+
join13(logDir, "engine.log"),
|
|
9353
9872
|
"--path",
|
|
9354
9873
|
projectPath,
|
|
9355
9874
|
"--import"
|
|
@@ -9371,13 +9890,13 @@ async function importOnce(godotPath, projectPath) {
|
|
|
9371
9890
|
}
|
|
9372
9891
|
async function runOperation(engine, operation, params, projectPath) {
|
|
9373
9892
|
const paramsDir = scratchDirectory("params");
|
|
9374
|
-
const paramsFile =
|
|
9375
|
-
const answerFile =
|
|
9893
|
+
const paramsFile = join13(paramsDir, `${operation}.json`);
|
|
9894
|
+
const answerFile = join13(paramsDir, "answer.json");
|
|
9376
9895
|
writeFileSync4(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
|
|
9377
9896
|
const args = [
|
|
9378
9897
|
"--headless",
|
|
9379
9898
|
"--log-file",
|
|
9380
|
-
|
|
9899
|
+
join13(paramsDir, "engine.log"),
|
|
9381
9900
|
"--path",
|
|
9382
9901
|
projectPath,
|
|
9383
9902
|
"--script",
|
|
@@ -9465,10 +9984,10 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
9465
9984
|
};
|
|
9466
9985
|
}
|
|
9467
9986
|
function answerIn(file) {
|
|
9468
|
-
if (!
|
|
9987
|
+
if (!existsSync11(file)) {
|
|
9469
9988
|
return { kind: "none" };
|
|
9470
9989
|
}
|
|
9471
|
-
const text =
|
|
9990
|
+
const text = readFileSync7(file, "utf8");
|
|
9472
9991
|
try {
|
|
9473
9992
|
const parsed = JSON.parse(text);
|
|
9474
9993
|
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? { kind: "answer", payload: parsed } : { kind: "unreadable", why: `it is JSON but not an object: ${text.slice(0, 200)}` };
|
|
@@ -9483,6 +10002,7 @@ var AFTER_ANSWER_NOTE = "Printed after this operation had written its answer, wh
|
|
|
9483
10002
|
var init_headless = __esm(() => {
|
|
9484
10003
|
init_doc_cache();
|
|
9485
10004
|
init_engine_run();
|
|
10005
|
+
init_import_freshness();
|
|
9486
10006
|
init_library_copies();
|
|
9487
10007
|
init_paths();
|
|
9488
10008
|
init_reimport();
|
|
@@ -9491,9 +10011,9 @@ var init_headless = __esm(() => {
|
|
|
9491
10011
|
});
|
|
9492
10012
|
|
|
9493
10013
|
// src/update-check.ts
|
|
9494
|
-
import { existsSync as
|
|
10014
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
|
|
9495
10015
|
import { homedir as homedir4, tmpdir as tmpdir3 } from "node:os";
|
|
9496
|
-
import { join as
|
|
10016
|
+
import { join as join14 } from "node:path";
|
|
9497
10017
|
function packageUrl(root) {
|
|
9498
10018
|
return `${root.replace(/\/+$/, "")}/gdharness/latest`;
|
|
9499
10019
|
}
|
|
@@ -9515,28 +10035,28 @@ function cacheDirectory(environment) {
|
|
|
9515
10035
|
};
|
|
9516
10036
|
const home = set("HOME") ?? homedir4();
|
|
9517
10037
|
if (process.platform === "win32") {
|
|
9518
|
-
return
|
|
10038
|
+
return join14(set("LOCALAPPDATA") ?? home, "gdharness");
|
|
9519
10039
|
}
|
|
9520
10040
|
if (process.platform === "darwin") {
|
|
9521
|
-
return
|
|
10041
|
+
return join14(home, "Library", "Caches", "gdharness");
|
|
9522
10042
|
}
|
|
9523
|
-
return
|
|
10043
|
+
return join14(set("XDG_CACHE_HOME") ?? join14(home, ".cache"), "gdharness");
|
|
9524
10044
|
}
|
|
9525
10045
|
function cacheFile(environment = process.env) {
|
|
9526
10046
|
try {
|
|
9527
10047
|
const directory = cacheDirectory(environment);
|
|
9528
10048
|
mkdirSync3(directory, { recursive: true, mode: 448 });
|
|
9529
|
-
return
|
|
10049
|
+
return join14(directory, "update-check.json");
|
|
9530
10050
|
} catch {
|
|
9531
|
-
return
|
|
10051
|
+
return join14(tmpdir3(), "gdharness-update-check.json");
|
|
9532
10052
|
}
|
|
9533
10053
|
}
|
|
9534
10054
|
function readCache(path) {
|
|
9535
10055
|
try {
|
|
9536
|
-
if (!
|
|
10056
|
+
if (!existsSync12(path)) {
|
|
9537
10057
|
return null;
|
|
9538
10058
|
}
|
|
9539
|
-
const parsed = JSON.parse(
|
|
10059
|
+
const parsed = JSON.parse(readFileSync8(path, "utf8"));
|
|
9540
10060
|
if (typeof parsed !== "object" || parsed === null) {
|
|
9541
10061
|
return null;
|
|
9542
10062
|
}
|
|
@@ -9684,7 +10204,7 @@ var init_update_check = __esm(() => {
|
|
|
9684
10204
|
});
|
|
9685
10205
|
|
|
9686
10206
|
// src/server-version.ts
|
|
9687
|
-
import { readFileSync as
|
|
10207
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
9688
10208
|
function staleAddon(addonDigest, shippedDigest) {
|
|
9689
10209
|
return addonDigest !== shippedDigest;
|
|
9690
10210
|
}
|
|
@@ -9743,7 +10263,7 @@ var init_server_version = __esm(() => {
|
|
|
9743
10263
|
DEBUG_MODE = process.env["GDHARNESS_DEBUG"] === "1";
|
|
9744
10264
|
SERVER_VERSION = (() => {
|
|
9745
10265
|
try {
|
|
9746
|
-
const pkg = JSON.parse(
|
|
10266
|
+
const pkg = JSON.parse(readFileSync9(new URL("../package.json", import.meta.url), "utf8"));
|
|
9747
10267
|
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
9748
10268
|
} catch {
|
|
9749
10269
|
return "0.0.0";
|
|
@@ -9752,7 +10272,7 @@ var init_server_version = __esm(() => {
|
|
|
9752
10272
|
});
|
|
9753
10273
|
|
|
9754
10274
|
// src/issues.ts
|
|
9755
|
-
import { createHash as
|
|
10275
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
9756
10276
|
import process9 from "node:process";
|
|
9757
10277
|
function feedbackNotice(every) {
|
|
9758
10278
|
return JSON.stringify({
|
|
@@ -9771,7 +10291,7 @@ function runtime() {
|
|
|
9771
10291
|
function defectSignature(where, message) {
|
|
9772
10292
|
const normalised = `${where}
|
|
9773
10293
|
${message}`.toLowerCase().replaceAll("\\", "/").replaceAll(/(?:[a-z]:)?(?:\/+[\w.-]+){2,}/gu, "<path>").replaceAll(/\d+/gu, "0");
|
|
9774
|
-
return
|
|
10294
|
+
return createHash3("sha256").update(normalised).digest("hex").slice(0, 8);
|
|
9775
10295
|
}
|
|
9776
10296
|
function facts(where, message, godotVersion) {
|
|
9777
10297
|
return [
|
|
@@ -10528,8 +11048,8 @@ var init_gdscript_source = __esm(() => {
|
|
|
10528
11048
|
});
|
|
10529
11049
|
|
|
10530
11050
|
// src/class-cache.ts
|
|
10531
|
-
import { existsSync as
|
|
10532
|
-
import { join as
|
|
11051
|
+
import { existsSync as existsSync13, readdirSync as readdirSync8, readFileSync as readFileSync10, statSync as statSync7 } from "node:fs";
|
|
11052
|
+
import { join as join15, posix } from "node:path";
|
|
10533
11053
|
function isToolScript(source) {
|
|
10534
11054
|
return TOOL.test(codeOf(source));
|
|
10535
11055
|
}
|
|
@@ -10548,18 +11068,18 @@ function declaredClassOf(source) {
|
|
|
10548
11068
|
}
|
|
10549
11069
|
function eachScript(projectPath, each) {
|
|
10550
11070
|
const visit = (directory, prefix) => {
|
|
10551
|
-
if (
|
|
11071
|
+
if (existsSync13(join15(directory, ".gdignore"))) {
|
|
10552
11072
|
return;
|
|
10553
11073
|
}
|
|
10554
|
-
for (const entry of
|
|
11074
|
+
for (const entry of readdirSync8(directory, { withFileTypes: true })) {
|
|
10555
11075
|
if (entry.name.startsWith(".")) {
|
|
10556
11076
|
continue;
|
|
10557
11077
|
}
|
|
10558
|
-
const path =
|
|
11078
|
+
const path = join15(directory, entry.name);
|
|
10559
11079
|
if (entry.isDirectory()) {
|
|
10560
11080
|
visit(path, `${prefix}${entry.name}/`);
|
|
10561
11081
|
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
10562
|
-
each(`res://${prefix}${entry.name}`,
|
|
11082
|
+
each(`res://${prefix}${entry.name}`, readFileSync10(path, "utf8"));
|
|
10563
11083
|
}
|
|
10564
11084
|
}
|
|
10565
11085
|
};
|
|
@@ -10581,12 +11101,12 @@ function scriptsNaming(projectPath, classes) {
|
|
|
10581
11101
|
return naming.sort((a, b) => a.script.localeCompare(b.script));
|
|
10582
11102
|
}
|
|
10583
11103
|
function cachedClasses(projectPath) {
|
|
10584
|
-
const cache =
|
|
10585
|
-
if (!
|
|
11104
|
+
const cache = join15(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
11105
|
+
if (!existsSync13(cache)) {
|
|
10586
11106
|
return null;
|
|
10587
11107
|
}
|
|
10588
11108
|
const listed = new Map;
|
|
10589
|
-
const text =
|
|
11109
|
+
const text = readFileSync10(cache, "utf8");
|
|
10590
11110
|
for (const entry of text.matchAll(/"class":\s*&"([^"]+)"[\s\S]*?"path":\s*"([^"]+)"/g)) {
|
|
10591
11111
|
listed.set(entry[1] ?? "", entry[2] ?? "");
|
|
10592
11112
|
}
|
|
@@ -10879,14 +11399,14 @@ function uncachedForTheEngineNote(projectPath, types) {
|
|
|
10879
11399
|
}
|
|
10880
11400
|
const one = types.length === 1;
|
|
10881
11401
|
const named = types.map((type) => `${type.type} (${type.declaredIn})`).join(", ");
|
|
10882
|
-
const cacheExists =
|
|
11402
|
+
const cacheExists = existsSync13(join15(projectPath, ".godot", "global_script_class_cache.cfg"));
|
|
10883
11403
|
const state = cacheExists ? `missing from .godot/global_script_class_cache.cfg, which is where the engine that compiled these scripts resolves a class name from, so the error is the cache being behind the files rather than the script.` : `the engine that compiled these scripts resolves a class name from .godot/global_script_class_cache.cfg, which has not been written yet, as in a project no editor has opened, so the error is the missing cache rather than the script.`;
|
|
10884
11404
|
return `${named} ${one ? "is" : "are"} declared in this project${cacheExists ? " and" : ", but"} ${state} ` + "project_import refresh_classes writes the cache from the declarations on disk; editor_run start " + "writes it before a game, and an editor opening the project writes it when it scans.";
|
|
10885
11405
|
}
|
|
10886
11406
|
function cacheWrittenAt(projectPath) {
|
|
10887
|
-
const cache =
|
|
11407
|
+
const cache = join15(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
10888
11408
|
try {
|
|
10889
|
-
return
|
|
11409
|
+
return statSync7(cache).mtimeMs;
|
|
10890
11410
|
} catch {
|
|
10891
11411
|
return null;
|
|
10892
11412
|
}
|
|
@@ -10898,14 +11418,14 @@ function declaredSince(projectPath, paths, names, since) {
|
|
|
10898
11418
|
return false;
|
|
10899
11419
|
}
|
|
10900
11420
|
try {
|
|
10901
|
-
return
|
|
11421
|
+
return statSync7(join15(projectPath, path.slice("res://".length))).mtimeMs > since;
|
|
10902
11422
|
} catch {
|
|
10903
11423
|
return false;
|
|
10904
11424
|
}
|
|
10905
11425
|
});
|
|
10906
11426
|
}
|
|
10907
11427
|
function cachedAtMissingPaths(cached, projectPath) {
|
|
10908
|
-
return [...cached].filter(([, path]) => path.startsWith("res://") && !
|
|
11428
|
+
return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync13(join15(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
|
|
10909
11429
|
}
|
|
10910
11430
|
function staleAgainst(cached, projectPath) {
|
|
10911
11431
|
return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
|
|
@@ -10929,314 +11449,6 @@ var init_class_cache = __esm(() => {
|
|
|
10929
11449
|
TOOL = /^[ \t]*(?:@[a-z_]+(?:\([^)\n]*\))?[ \t]+)*@tool\b/m;
|
|
10930
11450
|
});
|
|
10931
11451
|
|
|
10932
|
-
// src/project-scan.ts
|
|
10933
|
-
import { existsSync as existsSync12, readdirSync as readdirSync7, readFileSync as readFileSync9 } from "node:fs";
|
|
10934
|
-
import { extname, join as join14 } from "node:path";
|
|
10935
|
-
function skipped(name) {
|
|
10936
|
-
return name.startsWith(".") || SKIPPED.has(name);
|
|
10937
|
-
}
|
|
10938
|
-
function steppedOver(directory) {
|
|
10939
|
-
return gdignored(directory) || installedByUs(directory);
|
|
10940
|
-
}
|
|
10941
|
-
function gdignored(directory) {
|
|
10942
|
-
return existsSync12(join14(directory, ".gdignore"));
|
|
10943
|
-
}
|
|
10944
|
-
function installedByUs(directory) {
|
|
10945
|
-
return existsSync12(join14(directory, VERSION_MARKER));
|
|
10946
|
-
}
|
|
10947
|
-
function projectStructure(projectPath) {
|
|
10948
|
-
const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
|
|
10949
|
-
const visit = (directory) => {
|
|
10950
|
-
if (steppedOver(directory)) {
|
|
10951
|
-
return;
|
|
10952
|
-
}
|
|
10953
|
-
for (const entry of readdirSync7(directory, { withFileTypes: true })) {
|
|
10954
|
-
if (skipped(entry.name)) {
|
|
10955
|
-
continue;
|
|
10956
|
-
}
|
|
10957
|
-
if (entry.isDirectory()) {
|
|
10958
|
-
visit(join14(directory, entry.name));
|
|
10959
|
-
} else if (entry.isFile()) {
|
|
10960
|
-
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
10961
|
-
if (SCENE_EXTENSIONS.has(extension)) {
|
|
10962
|
-
structure.scenes += 1;
|
|
10963
|
-
} else if (extension === "gd" || extension === "cs") {
|
|
10964
|
-
structure.scripts += 1;
|
|
10965
|
-
} else if (ASSET_EXTENSIONS.has(extension)) {
|
|
10966
|
-
structure.assets += 1;
|
|
10967
|
-
} else {
|
|
10968
|
-
structure.other += 1;
|
|
10969
|
-
}
|
|
10970
|
-
}
|
|
10971
|
-
}
|
|
10972
|
-
};
|
|
10973
|
-
visit(projectPath);
|
|
10974
|
-
return structure;
|
|
10975
|
-
}
|
|
10976
|
-
function projectFiles(projectPath, extensions) {
|
|
10977
|
-
const found = [];
|
|
10978
|
-
const visit = (directory, prefix) => {
|
|
10979
|
-
if (steppedOver(directory)) {
|
|
10980
|
-
return;
|
|
10981
|
-
}
|
|
10982
|
-
for (const entry of readdirSync7(directory, { withFileTypes: true })) {
|
|
10983
|
-
if (skipped(entry.name)) {
|
|
10984
|
-
continue;
|
|
10985
|
-
}
|
|
10986
|
-
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
10987
|
-
if (entry.isDirectory()) {
|
|
10988
|
-
visit(join14(directory, entry.name), spelled);
|
|
10989
|
-
} else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
|
|
10990
|
-
found.push(spelled);
|
|
10991
|
-
}
|
|
10992
|
-
}
|
|
10993
|
-
};
|
|
10994
|
-
visit(projectPath, "");
|
|
10995
|
-
return found.sort();
|
|
10996
|
-
}
|
|
10997
|
-
function scriptsWithoutUid(projectPath) {
|
|
10998
|
-
const found = [];
|
|
10999
|
-
const visit = (directory, prefix) => {
|
|
11000
|
-
if (steppedOver(directory)) {
|
|
11001
|
-
return;
|
|
11002
|
-
}
|
|
11003
|
-
for (const entry of readdirSync7(directory, { withFileTypes: true })) {
|
|
11004
|
-
if (skipped(entry.name)) {
|
|
11005
|
-
continue;
|
|
11006
|
-
}
|
|
11007
|
-
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
11008
|
-
if (entry.isDirectory()) {
|
|
11009
|
-
visit(join14(directory, entry.name), spelled);
|
|
11010
|
-
continue;
|
|
11011
|
-
}
|
|
11012
|
-
if (!entry.isFile()) {
|
|
11013
|
-
continue;
|
|
11014
|
-
}
|
|
11015
|
-
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
11016
|
-
if (CARRIES_A_UID.has(extension) && !existsSync12(join14(directory, `${entry.name}.uid`))) {
|
|
11017
|
-
found.push(spelled);
|
|
11018
|
-
}
|
|
11019
|
-
}
|
|
11020
|
-
};
|
|
11021
|
-
visit(projectPath, "");
|
|
11022
|
-
return found.sort();
|
|
11023
|
-
}
|
|
11024
|
-
function pathsInUidCache(projectPath) {
|
|
11025
|
-
let data;
|
|
11026
|
-
try {
|
|
11027
|
-
data = readFileSync9(join14(projectPath, ".godot", "uid_cache.bin"));
|
|
11028
|
-
} catch {
|
|
11029
|
-
return null;
|
|
11030
|
-
}
|
|
11031
|
-
if (data.length < 4) {
|
|
11032
|
-
return null;
|
|
11033
|
-
}
|
|
11034
|
-
const count = data.readUInt32LE(0);
|
|
11035
|
-
const paths = new Map;
|
|
11036
|
-
let at = 4;
|
|
11037
|
-
for (let entry = 0;entry < count; entry++) {
|
|
11038
|
-
if (at + 12 > data.length) {
|
|
11039
|
-
return null;
|
|
11040
|
-
}
|
|
11041
|
-
const id = data.readBigUInt64LE(at);
|
|
11042
|
-
const length = data.readUInt32LE(at + 8);
|
|
11043
|
-
at += 12;
|
|
11044
|
-
if (at + length > data.length) {
|
|
11045
|
-
return null;
|
|
11046
|
-
}
|
|
11047
|
-
paths.set(id, data.toString("utf8", at, at + length));
|
|
11048
|
-
at += length;
|
|
11049
|
-
}
|
|
11050
|
-
return new Set(paths.values());
|
|
11051
|
-
}
|
|
11052
|
-
function isBinary(bytes) {
|
|
11053
|
-
return bytes.subarray(0, 8000).includes(0);
|
|
11054
|
-
}
|
|
11055
|
-
function projectTexts(projectPath) {
|
|
11056
|
-
const found = [];
|
|
11057
|
-
const visit = (directory, prefix) => {
|
|
11058
|
-
if (steppedOver(directory)) {
|
|
11059
|
-
return;
|
|
11060
|
-
}
|
|
11061
|
-
for (const entry of readdirSync7(directory, { withFileTypes: true })) {
|
|
11062
|
-
if (skipped(entry.name)) {
|
|
11063
|
-
continue;
|
|
11064
|
-
}
|
|
11065
|
-
const absolute = join14(directory, entry.name);
|
|
11066
|
-
const spelled = `${prefix}${entry.name}`;
|
|
11067
|
-
if (entry.isDirectory()) {
|
|
11068
|
-
visit(absolute, `${spelled}/`);
|
|
11069
|
-
continue;
|
|
11070
|
-
}
|
|
11071
|
-
const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
|
|
11072
|
-
if (!entry.isFile() || BINARY_EXTENSIONS.has(extension)) {
|
|
11073
|
-
continue;
|
|
11074
|
-
}
|
|
11075
|
-
const bytes = readFileSync9(absolute);
|
|
11076
|
-
if (!isBinary(bytes)) {
|
|
11077
|
-
found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
|
|
11078
|
-
}
|
|
11079
|
-
}
|
|
11080
|
-
};
|
|
11081
|
-
visit(projectPath, "");
|
|
11082
|
-
return found;
|
|
11083
|
-
}
|
|
11084
|
-
function ignoredTexts(projectPath) {
|
|
11085
|
-
const found = [];
|
|
11086
|
-
const visit = (directory, prefix, ignored) => {
|
|
11087
|
-
if (installedByUs(directory)) {
|
|
11088
|
-
return;
|
|
11089
|
-
}
|
|
11090
|
-
const under = ignored || gdignored(directory);
|
|
11091
|
-
for (const entry of readdirSync7(directory, { withFileTypes: true })) {
|
|
11092
|
-
if (skipped(entry.name)) {
|
|
11093
|
-
continue;
|
|
11094
|
-
}
|
|
11095
|
-
const absolute = join14(directory, entry.name);
|
|
11096
|
-
const spelled = `${prefix}${entry.name}`;
|
|
11097
|
-
if (entry.isDirectory()) {
|
|
11098
|
-
visit(absolute, `${spelled}/`, under);
|
|
11099
|
-
continue;
|
|
11100
|
-
}
|
|
11101
|
-
const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
|
|
11102
|
-
if (!under || !entry.isFile() || BINARY_EXTENSIONS.has(extension) || entry.name === ".gdignore") {
|
|
11103
|
-
continue;
|
|
11104
|
-
}
|
|
11105
|
-
const bytes = readFileSync9(absolute);
|
|
11106
|
-
if (!isBinary(bytes)) {
|
|
11107
|
-
found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
|
|
11108
|
-
}
|
|
11109
|
-
}
|
|
11110
|
-
};
|
|
11111
|
-
visit(projectPath, "", false);
|
|
11112
|
-
return found;
|
|
11113
|
-
}
|
|
11114
|
-
function plainMatch(line, needle, caseSensitive) {
|
|
11115
|
-
if (caseSensitive) {
|
|
11116
|
-
return line.includes(needle) ? [needle] : null;
|
|
11117
|
-
}
|
|
11118
|
-
const lowered = line.toLowerCase();
|
|
11119
|
-
const at = lowered.indexOf(needle);
|
|
11120
|
-
if (at < 0) {
|
|
11121
|
-
return null;
|
|
11122
|
-
}
|
|
11123
|
-
return [lowered.length === line.length ? line.slice(at, at + needle.length) : needle];
|
|
11124
|
-
}
|
|
11125
|
-
function compiledQuery(query, caseSensitive) {
|
|
11126
|
-
try {
|
|
11127
|
-
return new RegExp(query, caseSensitive ? "" : "i");
|
|
11128
|
-
} catch (error) {
|
|
11129
|
-
const why = errorMessage(error).split(": ").at(-1) ?? "";
|
|
11130
|
-
throw new Error(`'${query}' is not a regular expression: ${why.charAt(0).toLowerCase()}${why.slice(1)}. With regex true, a character such as ( ) [ ] { } . * + ? | ^ $ or \\ stands for itself only after a backslash, as in \\(; or leave regex out to search for the text as written.`);
|
|
11131
|
-
}
|
|
11132
|
-
}
|
|
11133
|
-
function searchProject(projectPath, options) {
|
|
11134
|
-
const extensions = options.fileTypes === undefined ? null : new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
|
|
11135
|
-
const result = {
|
|
11136
|
-
query: options.query,
|
|
11137
|
-
results: [],
|
|
11138
|
-
summary: { files_searched: 0, files_with_matches: 0, total_matches: 0, truncated: false }
|
|
11139
|
-
};
|
|
11140
|
-
const regex = options.regex ? compiledQuery(options.query, options.caseSensitive) : null;
|
|
11141
|
-
const needle = options.caseSensitive ? options.query : options.query.toLowerCase();
|
|
11142
|
-
const full = () => result.summary.truncated;
|
|
11143
|
-
const visit = (directory) => {
|
|
11144
|
-
if (steppedOver(directory)) {
|
|
11145
|
-
return;
|
|
11146
|
-
}
|
|
11147
|
-
for (const entry of readdirSync7(directory, { withFileTypes: true })) {
|
|
11148
|
-
if (full()) {
|
|
11149
|
-
return;
|
|
11150
|
-
}
|
|
11151
|
-
if (skipped(entry.name)) {
|
|
11152
|
-
continue;
|
|
11153
|
-
}
|
|
11154
|
-
const entryPath = join14(directory, entry.name);
|
|
11155
|
-
if (entry.isDirectory()) {
|
|
11156
|
-
visit(entryPath);
|
|
11157
|
-
continue;
|
|
11158
|
-
}
|
|
11159
|
-
const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
|
|
11160
|
-
if (!entry.isFile() || extensions !== null && !extensions.has(extension)) {
|
|
11161
|
-
continue;
|
|
11162
|
-
}
|
|
11163
|
-
const bytes = readFileSync9(entryPath);
|
|
11164
|
-
if (extensions === null && isBinary(bytes)) {
|
|
11165
|
-
continue;
|
|
11166
|
-
}
|
|
11167
|
-
result.summary.files_searched += 1;
|
|
11168
|
-
const matches = [];
|
|
11169
|
-
for (const [index, line] of bytes.toString("utf8").split(/\r?\n/).entries()) {
|
|
11170
|
-
const found = regex ? regex.exec(line) : plainMatch(line, needle, options.caseSensitive);
|
|
11171
|
-
if (found === null) {
|
|
11172
|
-
continue;
|
|
11173
|
-
}
|
|
11174
|
-
if (result.summary.total_matches >= options.maxResults) {
|
|
11175
|
-
result.summary.truncated = true;
|
|
11176
|
-
break;
|
|
11177
|
-
}
|
|
11178
|
-
matches.push({ line: index + 1, content: line.trim(), match: found[0] });
|
|
11179
|
-
result.summary.total_matches += 1;
|
|
11180
|
-
}
|
|
11181
|
-
if (matches.length > 0) {
|
|
11182
|
-
const relativePath = entryPath.slice(projectPath.length + 1).replace(/\\/g, "/");
|
|
11183
|
-
result.results.push({ file: `res://${relativePath}`, matches });
|
|
11184
|
-
result.summary.files_with_matches += 1;
|
|
11185
|
-
}
|
|
11186
|
-
}
|
|
11187
|
-
};
|
|
11188
|
-
visit(projectPath);
|
|
11189
|
-
return result;
|
|
11190
|
-
}
|
|
11191
|
-
var SKIPPED, VERSION_MARKER = ".gdharness-version", SCENE_EXTENSIONS, ASSET_EXTENSIONS, CARRIES_A_UID, BINARY_EXTENSIONS;
|
|
11192
|
-
var init_project_scan = __esm(() => {
|
|
11193
|
-
init_errors();
|
|
11194
|
-
SKIPPED = new Set(["node_modules"]);
|
|
11195
|
-
SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
|
|
11196
|
-
ASSET_EXTENSIONS = new Set([
|
|
11197
|
-
"png",
|
|
11198
|
-
"jpg",
|
|
11199
|
-
"jpeg",
|
|
11200
|
-
"webp",
|
|
11201
|
-
"svg",
|
|
11202
|
-
"bmp",
|
|
11203
|
-
"tga",
|
|
11204
|
-
"exr",
|
|
11205
|
-
"hdr",
|
|
11206
|
-
"ktx",
|
|
11207
|
-
"dds",
|
|
11208
|
-
"ttf",
|
|
11209
|
-
"otf",
|
|
11210
|
-
"woff",
|
|
11211
|
-
"woff2",
|
|
11212
|
-
"fnt",
|
|
11213
|
-
"wav",
|
|
11214
|
-
"mp3",
|
|
11215
|
-
"ogg",
|
|
11216
|
-
"glb",
|
|
11217
|
-
"gltf",
|
|
11218
|
-
"obj",
|
|
11219
|
-
"fbx",
|
|
11220
|
-
"blend",
|
|
11221
|
-
"dae",
|
|
11222
|
-
"ogv",
|
|
11223
|
-
"gdshader",
|
|
11224
|
-
"shader"
|
|
11225
|
-
]);
|
|
11226
|
-
CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
|
|
11227
|
-
BINARY_EXTENSIONS = new Set([
|
|
11228
|
-
...[...ASSET_EXTENSIONS].filter((extension) => !["svg", "gltf", "gdshader", "shader", "fnt"].includes(extension)),
|
|
11229
|
-
"scn",
|
|
11230
|
-
"res",
|
|
11231
|
-
"zip",
|
|
11232
|
-
"pck",
|
|
11233
|
-
"exe",
|
|
11234
|
-
"dll",
|
|
11235
|
-
"so",
|
|
11236
|
-
"dylib"
|
|
11237
|
-
]);
|
|
11238
|
-
});
|
|
11239
|
-
|
|
11240
11452
|
// src/answer-json.ts
|
|
11241
11453
|
function answerJson(value) {
|
|
11242
11454
|
return JSON.stringify(value, null, 2).replace(SCALAR_ARRAY, (_whole, items) => `[${items.split(/,\n\s+/).join(", ")}]`);
|
|
@@ -11248,7 +11460,7 @@ var init_answer_json = __esm(() => {
|
|
|
11248
11460
|
});
|
|
11249
11461
|
|
|
11250
11462
|
// src/resources.ts
|
|
11251
|
-
import { readFileSync as
|
|
11463
|
+
import { readFileSync as readFileSync11, realpathSync as realpathSync2 } from "node:fs";
|
|
11252
11464
|
import { extname as extname2, resolve as resolve3 } from "node:path";
|
|
11253
11465
|
function ensureProjectPath(getProjectPath) {
|
|
11254
11466
|
const projectPath = getProjectPath();
|
|
@@ -11444,7 +11656,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
11444
11656
|
const parsedUri = parseGodotUri(uri);
|
|
11445
11657
|
if (parsedUri.kind === "project-info") {
|
|
11446
11658
|
const projectFilePath = resolveProjectFile(projectPath, "project.godot");
|
|
11447
|
-
const rawProject =
|
|
11659
|
+
const rawProject = readFileSync11(projectFilePath, "utf-8");
|
|
11448
11660
|
const parsedProject = parseProjectGodot(rawProject);
|
|
11449
11661
|
return {
|
|
11450
11662
|
mimeType: "application/json",
|
|
@@ -11453,7 +11665,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
11453
11665
|
}
|
|
11454
11666
|
const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
|
|
11455
11667
|
ensureAllowedExtension(parsedUri.kind, filePath);
|
|
11456
|
-
const text =
|
|
11668
|
+
const text = readFileSync11(filePath, "utf-8");
|
|
11457
11669
|
return {
|
|
11458
11670
|
mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
|
|
11459
11671
|
text
|
|
@@ -11594,19 +11806,19 @@ function readParams(params, key) {
|
|
|
11594
11806
|
}
|
|
11595
11807
|
|
|
11596
11808
|
// src/setup.ts
|
|
11597
|
-
import { createHash as
|
|
11809
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
11598
11810
|
import {
|
|
11599
11811
|
cpSync,
|
|
11600
|
-
existsSync as
|
|
11812
|
+
existsSync as existsSync14,
|
|
11601
11813
|
mkdirSync as mkdirSync4,
|
|
11602
|
-
readdirSync as
|
|
11603
|
-
readFileSync as
|
|
11814
|
+
readdirSync as readdirSync9,
|
|
11815
|
+
readFileSync as readFileSync12,
|
|
11604
11816
|
rmdirSync,
|
|
11605
11817
|
rmSync as rmSync5,
|
|
11606
|
-
statSync as
|
|
11818
|
+
statSync as statSync8,
|
|
11607
11819
|
writeFileSync as writeFileSync6
|
|
11608
11820
|
} from "node:fs";
|
|
11609
|
-
import { dirname as dirname8, join as
|
|
11821
|
+
import { dirname as dirname8, join as join16 } from "node:path";
|
|
11610
11822
|
import { fileURLToPath } from "node:url";
|
|
11611
11823
|
function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
11612
11824
|
return addonsDigest(EDITOR_PLUGINS, from);
|
|
@@ -11615,27 +11827,27 @@ function runtimeAddonDigest(from = shippedAddonsDirectory()) {
|
|
|
11615
11827
|
return addonsDigest([RUNTIME_ADDON], from);
|
|
11616
11828
|
}
|
|
11617
11829
|
function addonsDigest(names, from) {
|
|
11618
|
-
const hash =
|
|
11830
|
+
const hash = createHash4("sha256");
|
|
11619
11831
|
const walk = (directory, relative) => {
|
|
11620
|
-
for (const name of
|
|
11832
|
+
for (const name of readdirSync9(directory).sort()) {
|
|
11621
11833
|
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
11622
11834
|
continue;
|
|
11623
11835
|
}
|
|
11624
|
-
const path =
|
|
11836
|
+
const path = join16(directory, name);
|
|
11625
11837
|
const inside = `${relative}/${name}`;
|
|
11626
|
-
if (
|
|
11838
|
+
if (statSync8(path).isDirectory()) {
|
|
11627
11839
|
walk(path, inside);
|
|
11628
11840
|
} else {
|
|
11629
11841
|
hash.update(`${inside}
|
|
11630
11842
|
`);
|
|
11631
|
-
hash.update(
|
|
11843
|
+
hash.update(readFileSync12(path));
|
|
11632
11844
|
hash.update(`
|
|
11633
11845
|
`);
|
|
11634
11846
|
}
|
|
11635
11847
|
}
|
|
11636
11848
|
};
|
|
11637
11849
|
for (const name of names) {
|
|
11638
|
-
walk(
|
|
11850
|
+
walk(join16(from, name), name);
|
|
11639
11851
|
}
|
|
11640
11852
|
return hash.digest("hex");
|
|
11641
11853
|
}
|
|
@@ -11654,28 +11866,28 @@ function installedRuntimeDigest(projectPath) {
|
|
|
11654
11866
|
return digestMarker(projectPath, RUNTIME_ADDON);
|
|
11655
11867
|
}
|
|
11656
11868
|
function digestMarker(projectPath, addon) {
|
|
11657
|
-
const marker =
|
|
11658
|
-
if (!
|
|
11869
|
+
const marker = join16(projectPath, "addons", addon, DIGEST_MARKER);
|
|
11870
|
+
if (!existsSync14(marker)) {
|
|
11659
11871
|
return null;
|
|
11660
11872
|
}
|
|
11661
|
-
const said =
|
|
11873
|
+
const said = readFileSync12(marker, "utf8").trim();
|
|
11662
11874
|
return said === "" ? null : said;
|
|
11663
11875
|
}
|
|
11664
11876
|
function shippedAddonsDirectory() {
|
|
11665
|
-
return
|
|
11877
|
+
return join16(dirname8(fileURLToPath(import.meta.url)), "godot", "addons");
|
|
11666
11878
|
}
|
|
11667
11879
|
function shippedOperationsScript() {
|
|
11668
|
-
return
|
|
11880
|
+
return join16(dirname8(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
|
|
11669
11881
|
}
|
|
11670
11882
|
function shippedListedRunner() {
|
|
11671
|
-
return
|
|
11883
|
+
return join16(dirname8(fileURLToPath(import.meta.url)), "godot", "tests", "listed.gd");
|
|
11672
11884
|
}
|
|
11673
11885
|
function gdUnit4Unusable(projectPath) {
|
|
11674
11886
|
const marker = "addons/gdUnit4/plugin.cfg";
|
|
11675
|
-
if (!
|
|
11887
|
+
if (!existsSync14(join16(projectPath, ...marker.split("/")))) {
|
|
11676
11888
|
return { missing: true, reason: `gdUnit4 is not installed in this project: no ${marker}` };
|
|
11677
11889
|
}
|
|
11678
|
-
if (!
|
|
11890
|
+
if (!existsSync14(join16(projectPath, ...GDUNIT4_CI_RUNNER.split("/")))) {
|
|
11679
11891
|
return {
|
|
11680
11892
|
missing: false,
|
|
11681
11893
|
reason: `gdUnit4 in this project is older than 6.0, which the shipped runner needs: no ${GDUNIT4_CI_RUNNER}`
|
|
@@ -11686,44 +11898,44 @@ function gdUnit4Unusable(projectPath) {
|
|
|
11686
11898
|
function installAddons(projectPath, from = shippedAddonsDirectory()) {
|
|
11687
11899
|
const installed = [];
|
|
11688
11900
|
for (const name of ADDONS) {
|
|
11689
|
-
const source =
|
|
11690
|
-
if (!
|
|
11901
|
+
const source = join16(from, name);
|
|
11902
|
+
if (!existsSync14(join16(source, name === RUNTIME_ADDON ? "runtime_autoload.gd" : "plugin.cfg"))) {
|
|
11691
11903
|
throw new Error(`The package holds no ${name} addon at ${source}.`);
|
|
11692
11904
|
}
|
|
11693
|
-
const target =
|
|
11694
|
-
const replaced =
|
|
11905
|
+
const target = join16(projectPath, "addons", name);
|
|
11906
|
+
const replaced = existsSync14(target);
|
|
11695
11907
|
rmSync5(target, { recursive: true, force: true });
|
|
11696
11908
|
mkdirSync4(dirname8(target), { recursive: true });
|
|
11697
11909
|
cpSync(source, target, { recursive: true });
|
|
11698
|
-
writeFileSync6(
|
|
11910
|
+
writeFileSync6(join16(target, VERSION_MARKER), `${SERVER_VERSION}
|
|
11699
11911
|
`);
|
|
11700
11912
|
installed.push({ name, path: target, replaced });
|
|
11701
11913
|
}
|
|
11702
|
-
writeFileSync6(
|
|
11914
|
+
writeFileSync6(join16(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
|
|
11703
11915
|
`);
|
|
11704
|
-
writeFileSync6(
|
|
11916
|
+
writeFileSync6(join16(projectPath, "addons", RUNTIME_ADDON, DIGEST_MARKER), `${runtimeAddonDigest(from)}
|
|
11705
11917
|
`);
|
|
11706
11918
|
return installed;
|
|
11707
11919
|
}
|
|
11708
11920
|
function installedAddonVersion(projectPath) {
|
|
11709
|
-
const marker =
|
|
11710
|
-
if (!
|
|
11921
|
+
const marker = join16(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
11922
|
+
if (!existsSync14(marker)) {
|
|
11711
11923
|
return null;
|
|
11712
11924
|
}
|
|
11713
|
-
const said =
|
|
11925
|
+
const said = readFileSync12(marker, "utf8").trim();
|
|
11714
11926
|
return said === "" ? null : said;
|
|
11715
11927
|
}
|
|
11716
11928
|
function removeAddons(projectPath) {
|
|
11717
11929
|
const removed = [];
|
|
11718
11930
|
for (const name of ADDONS) {
|
|
11719
|
-
const target =
|
|
11720
|
-
if (
|
|
11931
|
+
const target = join16(projectPath, "addons", name);
|
|
11932
|
+
if (existsSync14(target)) {
|
|
11721
11933
|
rmSync5(target, { recursive: true, force: true });
|
|
11722
11934
|
removed.push(target);
|
|
11723
11935
|
}
|
|
11724
11936
|
}
|
|
11725
|
-
const addons =
|
|
11726
|
-
if (removed.length > 0 &&
|
|
11937
|
+
const addons = join16(projectPath, "addons");
|
|
11938
|
+
if (removed.length > 0 && existsSync14(addons) && readdirSync9(addons).length === 0) {
|
|
11727
11939
|
rmdirSync(addons);
|
|
11728
11940
|
removed.push(addons);
|
|
11729
11941
|
}
|
|
@@ -11757,7 +11969,7 @@ function loaderAmong(projectPath, named) {
|
|
|
11757
11969
|
}
|
|
11758
11970
|
let source = null;
|
|
11759
11971
|
try {
|
|
11760
|
-
source =
|
|
11972
|
+
source = readFileSync12(join16(projectPath, path), "utf8");
|
|
11761
11973
|
} catch {}
|
|
11762
11974
|
if (source?.includes(RUNTIME_AUTOLOAD.path)) {
|
|
11763
11975
|
return { name: entry, path, exists: true };
|
|
@@ -11799,10 +12011,10 @@ function enabledPlugins(settings) {
|
|
|
11799
12011
|
function inspectProject(projectPath) {
|
|
11800
12012
|
const problems = [];
|
|
11801
12013
|
const addons = ADDONS.map((name) => {
|
|
11802
|
-
const directory =
|
|
11803
|
-
const marker =
|
|
11804
|
-
const installed =
|
|
11805
|
-
const version =
|
|
12014
|
+
const directory = join16(projectPath, "addons", name);
|
|
12015
|
+
const marker = join16(directory, VERSION_MARKER);
|
|
12016
|
+
const installed = existsSync14(directory);
|
|
12017
|
+
const version = existsSync14(marker) ? readFileSync12(marker, "utf8").trim() : null;
|
|
11806
12018
|
const current = version === SERVER_VERSION;
|
|
11807
12019
|
if (!installed) {
|
|
11808
12020
|
problems.push(`addons/${name} is not installed; run gdharness setup`);
|
|
@@ -11811,7 +12023,7 @@ function inspectProject(projectPath) {
|
|
|
11811
12023
|
}
|
|
11812
12024
|
return { name, installed, version, current };
|
|
11813
12025
|
});
|
|
11814
|
-
const settings = parseProjectGodot(
|
|
12026
|
+
const settings = parseProjectGodot(readFileSync12(join16(projectPath, "project.godot"), "utf8"));
|
|
11815
12027
|
const pluginsEnabled = enabledPlugins(settings);
|
|
11816
12028
|
for (const name of EDITOR_PLUGINS) {
|
|
11817
12029
|
if (!pluginsEnabled.includes(name)) {
|
|
@@ -11827,7 +12039,7 @@ function inspectProject(projectPath) {
|
|
|
11827
12039
|
const tracked = trackedByGit(projectPath, [...named.values()]);
|
|
11828
12040
|
const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
|
|
11829
12041
|
for (const [name, path] of named) {
|
|
11830
|
-
if (!
|
|
12042
|
+
if (!existsSync14(join16(projectPath, path))) {
|
|
11831
12043
|
if (!addonMissing(path)) {
|
|
11832
12044
|
problems.push(`the ${name} autoload names res://${path}, which is not in this project, so it boots with a missing script; restore that file, or point the autoload at one that is here`);
|
|
11833
12045
|
}
|
|
@@ -11856,7 +12068,7 @@ function inspectProject(projectPath) {
|
|
|
11856
12068
|
problems
|
|
11857
12069
|
};
|
|
11858
12070
|
}
|
|
11859
|
-
var ADDONS, EDITOR_PLUGINS, RUNTIME_AUTOLOAD, SCRIPT_RUNS_SETTING = "gdharness/runtime/serve_script_runs", BIND_ADDRESS_SETTING = "gdharness/runtime/bind_address", PORT_SETTING = "gdharness/runtime/port", DIGEST_MARKER = ".gdharness-digest", RUNTIME_ADDON = "gdharness_runtime", shippedDigest, shippedRuntime, GDUNIT4_CI_RUNNER = "addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd";
|
|
12071
|
+
var ADDONS, EDITOR_PLUGINS, RUNTIME_AUTOLOAD, SCRIPT_RUNS_SETTING = "gdharness/runtime/serve_script_runs", QUIET_SCENES_SETTING = "gdharness/runtime/quiet_scenes_under", BIND_ADDRESS_SETTING = "gdharness/runtime/bind_address", PORT_SETTING = "gdharness/runtime/port", DIGEST_MARKER = ".gdharness-digest", RUNTIME_ADDON = "gdharness_runtime", shippedDigest, shippedRuntime, GDUNIT4_CI_RUNNER = "addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd";
|
|
11860
12072
|
var init_setup = __esm(() => {
|
|
11861
12073
|
init_class_cache();
|
|
11862
12074
|
init_headless();
|
|
@@ -11980,6 +12192,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11980
12192
|
init_launch();
|
|
11981
12193
|
PROJECT_INFO_SECTIONS = [
|
|
11982
12194
|
"autoloads",
|
|
12195
|
+
"classes",
|
|
11983
12196
|
"input_actions",
|
|
11984
12197
|
"plugins",
|
|
11985
12198
|
"export_presets",
|
|
@@ -12147,13 +12360,13 @@ var init_tool_definitions = __esm(() => {
|
|
|
12147
12360
|
name: "project_info",
|
|
12148
12361
|
title: "Project info",
|
|
12149
12362
|
effect: "read",
|
|
12150
|
-
description: "What a project is: its name and main scene from project.godot, the Godot that answers, and how many scenes, scripts and assets it holds, other counting every remaining file, leaving out the addons gdharness installed and any folder holding a .gdignore, with optional sections on top.",
|
|
12363
|
+
description: "What a project is: its name and main scene from project.godot, the Godot that answers, and how many scenes, scripts and assets it holds, other counting every remaining file, leaving out the .import and .uid files the engine writes beside others, the addons gdharness installed and any folder holding a .gdignore, with optional sections on top.",
|
|
12151
12364
|
parameters: {
|
|
12152
12365
|
projectPath: PROJECT_PATH,
|
|
12153
12366
|
include: {
|
|
12154
12367
|
type: "array",
|
|
12155
12368
|
items: { type: "string", enum: [...PROJECT_INFO_SECTIONS] },
|
|
12156
|
-
description: "Extra sections: registered autoloads, the input actions project.godot holds with their events (an engine ui_ action only when the project rebinds it, as engineAction), addons and whether each is enabled, export presets, the audio bus layout, a health report, or export validation. health and validation compile every script of the project in headless engines and name each that fails with its first error, and each file a scene, resource or script refers to that does not exist."
|
|
12369
|
+
description: "Extra sections: registered autoloads, the classes the project's scripts declare with class_name, each with its script, read from the files, the input actions project.godot holds with their events (an engine ui_ action only when the project rebinds it, as engineAction), addons and whether each is enabled, export presets, the audio bus layout, a health report, or export validation. health and validation compile every script of the project in headless engines and name each that fails with its first error, and each file a scene, resource or script refers to that does not exist."
|
|
12157
12370
|
},
|
|
12158
12371
|
preset: { type: "string", description: "For validation: the export preset to validate against." },
|
|
12159
12372
|
detail: {
|
|
@@ -12298,7 +12511,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12298
12511
|
name: "project_search",
|
|
12299
12512
|
title: "Search the project",
|
|
12300
12513
|
effect: "read",
|
|
12301
|
-
description: "Searches text or a regular expression across project files and returns file paths with line numbers. What the engine steps over is not searched: directories spelled with a dot, node_modules, and any directory holding a .gdignore. A vendored engine or an export directory is therefore absent from the results rather than matching the code you are looking for in a copy nobody runs. The addons gdharness installed are not searched either, since nobody on the project wrote them.",
|
|
12514
|
+
description: "Searches text or a regular expression across project files and returns file paths with line numbers. What the engine steps over is not searched: directories spelled with a dot, node_modules, and any directory holding a .gdignore, which not_searched names when the search passed one. A vendored engine or an export directory is therefore absent from the results rather than matching the code you are looking for in a copy nobody runs. The addons gdharness installed are not searched either, since nobody on the project wrote them.",
|
|
12302
12515
|
parameters: {
|
|
12303
12516
|
projectPath: PROJECT_PATH,
|
|
12304
12517
|
query: { type: "string", description: "The text or pattern to find." },
|
|
@@ -12324,7 +12537,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12324
12537
|
direction: {
|
|
12325
12538
|
type: "string",
|
|
12326
12539
|
enum: ["forward", "reverse"],
|
|
12327
|
-
description: "forward: what this resource loads, with cycles reported, a script's dependencies including every script whose class_name it uses, marked by_class with the name. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name. Each reference carries a kind: ext_resource, extends, preload, load or path for a reference by path; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about. Two kinds are mentions rather than uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs, and comment is one in ordinary prose, which it would only make stale. summary.in_code is the count with both of those left out, which is the number to read when the question is whether anything still uses this. Default forward."
|
|
12540
|
+
description: "forward: what this resource loads, with cycles reported, a script's dependencies including every script whose class_name it uses, marked by_class with the name, and the script of every autoload it names, marked by_autoload with the autoload's name. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name, and for an autoload's script every use of the autoload's name, with autoload naming it. Each reference carries a kind: ext_resource, extends, preload, load or path for a reference by path; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about; autoload for code naming the autoload. Two kinds are mentions rather than uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs, and comment is one in ordinary prose, which it would only make stale. summary.in_code is the count with both of those left out, which is the number to read when the question is whether anything still uses this. Default forward."
|
|
12328
12541
|
},
|
|
12329
12542
|
depth: { type: "number", description: "forward: how many levels to follow. Default unlimited." },
|
|
12330
12543
|
includeBuiltin: {
|
|
@@ -12354,7 +12567,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12354
12567
|
options: {
|
|
12355
12568
|
type: "object",
|
|
12356
12569
|
ops: ["set_options"],
|
|
12357
|
-
description: `set_options: import options keyed as the .import file spells them, {"compress/mode": 1}. A dictionary option whose keys are numbers, a font's opentype_features or any option already holding number keys, takes them written as strings of digits, and opentype_features also takes an OpenType tag by its four-letter name: {"opentype_features": {"liga": 0}} turns ligatures off. Both are written as the numbers the engine reads. One already holding the value given is answered under unchanged_options rather than updated_options, and when none changed nothing is written or reimported.`,
|
|
12570
|
+
description: `set_options: import options keyed as the .import file spells them, {"compress/mode": 1}. A dictionary option whose keys are numbers, a font's opentype_features or any option already holding number keys, takes them written as strings of digits, and opentype_features also takes an OpenType tag by its four-letter name: {"opentype_features": {"liga": 0}} turns ligatures off. Both are written as the numbers the engine reads. One already holding the value given is answered under unchanged_options rather than updated_options, and when none changed nothing is written or reimported. changed gives each option written as {was, now}, was null for one the import file did not hold.`,
|
|
12358
12571
|
additionalProperties: true
|
|
12359
12572
|
},
|
|
12360
12573
|
reimport: {
|
|
@@ -12412,7 +12625,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12412
12625
|
{
|
|
12413
12626
|
name: "project_export",
|
|
12414
12627
|
title: "Export",
|
|
12415
|
-
description: "Export presets and exports. add_preset writes a preset to export_presets.cfg with the keys the editor's Project > Export writes for a new one, every platform option left at its default, and makes it the platform's runnable preset when the platform has none. It answers where this engine's export templates belong and whether the preset's platform has them, since an export needs them; list says the same for every preset. An editor open on the project does not see a preset added this way until it restarts. run names, as alsoWritten, what else the export wrote beside its output, such as the .pck a Windows or Linux build cannot start without.",
|
|
12628
|
+
description: "Export presets and exports. add_preset writes a preset to export_presets.cfg with the keys the editor's Project > Export writes for a new one, every platform option left at its default, and makes it the platform's runnable preset when the platform has none. It answers where this engine's export templates belong and whether the preset's platform has them, since an export needs them; list says the same for every preset. An editor open on the project does not see a preset added this way until it restarts. run names, as alsoWritten, what else the export wrote beside its output, such as the .pck a Windows or Linux build cannot start without. Its entries are the errors and warnings the export printed, the newest two hundred distinct ones, one printed more than once carrying times, and errors and warnings count them; exported is true only when this run wrote the output and printed no error.",
|
|
12416
12629
|
parameters: {
|
|
12417
12630
|
projectPath: PROJECT_PATH,
|
|
12418
12631
|
preset: {
|
|
@@ -12448,19 +12661,19 @@ var init_tool_definitions = __esm(() => {
|
|
|
12448
12661
|
name: "project_test",
|
|
12449
12662
|
title: "Run tests",
|
|
12450
12663
|
effect: "additive",
|
|
12451
|
-
description: "Runs the project's gdUnit4 tests headless 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
|
|
12452
|
-
answer: "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. " + IMPORTED_FIRST,
|
|
12664
|
+
description: "Runs the project's gdUnit4 tests headless 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, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it 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. 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. 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 nodes were left in the tree 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.",
|
|
12665
|
+
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. The run is judged as gdharness test judges it: undeclaredErrors names each engine error that no suite declared in its GDHARNESS_EXPECTED_ERRORS constant, with the suite and the case that printed it, and declaredNotPrinted each declared error its case did not print. Either fails the run, and each suite holding one is listed with its count. engineErrors counts every engine error the run printed but those a suite declared. " + IMPORTED_FIRST,
|
|
12453
12666
|
parameters: {
|
|
12454
12667
|
projectPath: PROJECT_PATH,
|
|
12455
12668
|
path: {
|
|
12456
12669
|
type: ["string", "array"],
|
|
12457
12670
|
items: { type: "string" },
|
|
12458
|
-
description: "A test directory or suite file inside the project, or a list of them, all run on one engine: a named set of suites boots once rather than once per suite. An entry given twice, or a suite inside a directory also listed, runs once. An entry that is not there is left out and named under missing, the rest run, and such a run is never called passed; when no entry is there the run is refused before the engine starts, naming each one. Default test."
|
|
12671
|
+
description: "A test directory or suite file inside the project, or a list of them, all run on one engine: a named set of suites boots once rather than once per suite. An entry given twice, or a suite inside a directory also listed, runs once. An entry that is not there is left out and named under missing, the rest run, and such a run is never called passed; when no entry is there the run is refused before the engine starts, naming each one. Default tests and test, those the project has, which is where gdharness test looks."
|
|
12459
12672
|
},
|
|
12460
12673
|
ignore: {
|
|
12461
12674
|
type: "array",
|
|
12462
12675
|
items: { type: "string" },
|
|
12463
|
-
description: 'Suites or cases to leave out, as "suite_name" or "suite_name:test_name". A bare test name is read as a suite name and leaves out nothing; an entry that named a case which ran all the same comes back under ignoreNotApplied with the form that would have left it out.'
|
|
12676
|
+
description: 'Suites or cases to leave out, as "suite_name" or "suite_name:test_name". A bare test name is read as a suite name and leaves out nothing; an entry that named a case which ran all the same comes back under ignoreNotApplied with the form that would have left it out. Every case the list left out comes back under ignored as suite_name:test_name, so a run called passed says what it did not run, and an entry naming no suite or case under path comes back under ignoreUnmatched.'
|
|
12464
12677
|
},
|
|
12465
12678
|
tests: {
|
|
12466
12679
|
type: "array",
|
|
@@ -12696,7 +12909,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12696
12909
|
{
|
|
12697
12910
|
name: "scene_animation",
|
|
12698
12911
|
title: "Animations",
|
|
12699
|
-
description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. 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 past the animation's end, where it never plays, is written and named under pastLength. remove clears the player's autoplay when it named the animation and names under statesPlayingNothing the states that played it. 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.",
|
|
12912
|
+
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 past the animation's end, where it never plays, is written and named under pastLength. remove clears the player's autoplay when it named the animation and names under statesPlayingNothing the states that played it. 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.",
|
|
12700
12913
|
parameters: {
|
|
12701
12914
|
projectPath: PROJECT_PATH,
|
|
12702
12915
|
scenePath: SCENE_PATH,
|
|
@@ -12904,7 +13117,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12904
13117
|
operations: {
|
|
12905
13118
|
structure: {
|
|
12906
13119
|
effect: "read",
|
|
12907
|
-
summary: "functions, variables (static ones marked is_static; one written with := has the type of a literal or built-in constructor it is given, marked inferred, as a constant always does), signals, constants, enums, inner_classes (each with its name, line and extends, and its own members in these same fields), class_name, extends and line_count, read from the file, and dependencies: the paths it loads or preloads as written, then the scripts declaring each project class_name its code uses, which classes_used names with the class; autoloads_used names each autoload the code uses by its global name, with the autoload's script, which the project loads rather than this script, so it is not under dependencies",
|
|
13120
|
+
summary: "functions, variables (static ones marked is_static; one written with := has the type of a literal or built-in constructor it is given, marked inferred, as a constant always does), signals, constants, enums, inner_classes (each with its name, line and extends, and its own members in these same fields), class_name, extends and line_count, read from the file, and dependencies: the paths it loads or preloads as written, then the scripts declaring each project class_name its code uses, which classes_used names with the class; autoloads_used names each autoload the code uses by its global name, with the autoload's script, which the project loads rather than this script, so it is not under dependencies. A member or inner class with a ## documentation comment directly above it carries the comment's text as doc, and the script's own, the ## block above class_name or extends or one followed by a blank line before any member, comes back as class_doc",
|
|
12908
13121
|
requires: []
|
|
12909
13122
|
},
|
|
12910
13123
|
symbols: {
|
|
@@ -13042,7 +13255,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
13042
13255
|
name: "editor_launch",
|
|
13043
13256
|
title: "Open, restart or close the editor, or bind its debug adapter again",
|
|
13044
13257
|
description: "Opens the Godot editor on a project, or restarts or closes the one already connected, or binds its debug adapter again. close saves open scenes, quits the editor and waits for its process to go, which is the only way to end a hidden one short of killing it; an editor this server opened that has not connected yet has nothing of this server's to save, so close stops its process instead. open answers once the process has started, and the editor dials in after its import; a call needing the editor made before then waits for the one this server opened, for as long as a restart waits, and is refused naming it if it has not arrived. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. Godot tries each server port once, as the editor starts, so a restart binds a debug adapter that came back without its port again, in the editor that came back. rebind does the same for the connected editor without restarting it, whoever opened it; a debug session open on the adapter ends with it and is opened again by the next debug call. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so in editor_status rather than waiting for an editor that is not coming, and open finishes it. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
|
|
13045
|
-
answer: "close answers settingsDropped, naming what its save took out of project.godot and left out when it took nothing, and settingsNote when the file could not be read to tell; an editor stopped before it connected is answered with beforeConnecting. restart answers with the version that came back, and says under settingsDropped which keys its save dropped, because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart answers lspListening and dapListening once the editor's servers are up
|
|
13258
|
+
answer: "close answers settingsDropped, naming what its save took out of project.godot and left out when it took nothing, and settingsNote when the file could not be read to tell; an editor stopped before it connected is answered with beforeConnecting. close and restart name under savedScenes the open scenes that had unsaved changes and were saved on the way out, an empty list when none had; a scene that would not save refuses the close or restart and leaves the editor running. Both answer tookMs, how long the editor took to go or to come back. restart answers with the version that came back, and says under settingsDropped which keys its save dropped, because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart answers lspListening and dapListening once the editor's servers are up. When either came back without its port, serversSaid gives the lines the editor's log panel has about its servers, the one place Godot says why a port would not bind, and when it was the debug adapter, which is bound again in the editor that came back, debugAdapterRebound says whether that took; neither is there when both came up. Godot clears that panel whenever a game is played, unless its run/output/always_clear_output_on_play setting is off, so after a play the lines from the editor's own start are gone. rebind answers rebound, with lspListening, dapListening and serversSaid, the lines that rebind made the panel print. A restart that a server's end interrupted shows as restartInterrupted in editor_status, and the open that finishes it says so under finishesRestart.",
|
|
13046
13259
|
parameters: {
|
|
13047
13260
|
projectPath: { ...PROJECT_PATH, ops: ["open"] },
|
|
13048
13261
|
hidden: {
|
|
@@ -13595,7 +13808,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
13595
13808
|
},
|
|
13596
13809
|
outputPath: {
|
|
13597
13810
|
type: "string",
|
|
13598
|
-
description: "Also save the picture as a PNG at this absolute path, for somebody to open later; the answer says where. Refused unless it ends in .png, its directory exists, it is outside the project, and no file is there already."
|
|
13811
|
+
description: "Also save the picture as a lossless PNG at this absolute path, for somebody to open later or a script to compare; the answer says where. Refused unless it ends in .png, its directory exists, it is outside the project, and no file is there already."
|
|
13812
|
+
},
|
|
13813
|
+
inline: {
|
|
13814
|
+
type: "boolean",
|
|
13815
|
+
description: "Send the picture back in the answer. Default true, as a JPEG when that is smaller than the PNG and the picture has no transparency (the text says so, with both sizes), since every picture an answer carries stays in the session for as long as it lasts. false sends none and needs outputPath, for a capture a script compares rather than one somebody looks at."
|
|
13599
13816
|
}
|
|
13600
13817
|
},
|
|
13601
13818
|
requires: [],
|
|
@@ -13609,7 +13826,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
13609
13826
|
name: "runtime_input",
|
|
13610
13827
|
title: "Send input to the game",
|
|
13611
13828
|
description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button, mouse motion or drag. All of it works headless, where the viewport is the project's window size and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position. A native window no real pointer has entered gets no hover from Godot, so the runtime supplies it there and the answers say hover_supplied.",
|
|
13612
|
-
answer: `click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, its index being the index argument that presses the same match again, found.item names the tab, list entry, tree row or menu bar title pressed (kind; index, the item's own number in its control, which is not found.index; says; and for a row the column of the cell saying the words), and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again and pressed whether it is left held down (true only after pressed true), for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. mouse_drag: from, to, motions sent and relative_total. joypad_motion: the axis by number and name, the value and the device. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer. A mouse_click, mouse_motion or mouse_drag with a point outside the window's viewport says so under outside_viewport, with the viewport's rectangle, since no control there receives it.`,
|
|
13829
|
+
answer: `click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, its index being the index argument that presses the same match again, found.item names the tab, list entry, tree row or menu bar title pressed (kind; index, the item's own number in its control, which is not found.index; says; and for a row the column of the cell saying the words), and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again and pressed whether it is left held down (true only after pressed true), for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. mouse_drag: from, to, motions sent and relative_total. joypad_motion: the axis by number and name, the value and the device. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer. An action or a key answers focused with the path of the Control holding the keyboard focus when it went in, since that Control gets it before the game's _unhandled_input: Space or Enter on a focused button presses the button. A mouse_click, mouse_motion or mouse_drag with a point outside the window's viewport says so under outside_viewport, with the viewport's rectangle, since no control there receives it.`,
|
|
13613
13830
|
parameters: {
|
|
13614
13831
|
projectPath: RUNNING_PROJECT_PATH,
|
|
13615
13832
|
pid: RUNNING_PID,
|
|
@@ -13897,7 +14114,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
13897
14114
|
operations: {
|
|
13898
14115
|
pause: {
|
|
13899
14116
|
effect: "additive",
|
|
13900
|
-
summary: "stop a game this server started where it is, answering with the stack. The game takes it between frames, and in a script every 2048 lines it runs, so a loop that sleeps between few lines can take seconds; the call waits thirty, and past that answers paused false with the request still standing. Stopped between frames, where no script runs, the game is stepped on to the next line of script it runs",
|
|
14117
|
+
summary: "stop a game this server started where it is, answering with the stack. The game takes it between frames, and in a script every 2048 lines it runs, so a loop that sleeps between few lines can take seconds; the call waits thirty, and past that answers paused false with the request still standing. Stopped between frames, where no script runs, the game is stepped on to the next line of script it runs, and stopped in gdharness's runtime addon, an autoload whose _process runs first in every frame, on to the next line of the game's own script, for up to five seconds; one whose stack is still the addon's after them says so in a note",
|
|
13901
14118
|
requires: []
|
|
13902
14119
|
},
|
|
13903
14120
|
continue: { effect: "additive", summary: "resume after a breakpoint", requires: [] },
|
|
@@ -14045,9 +14262,9 @@ function whatItCanChange(tool) {
|
|
|
14045
14262
|
if (destroying.length === 0) {
|
|
14046
14263
|
return "Adds or starts things, and overwrites and removes nothing itself.";
|
|
14047
14264
|
}
|
|
14048
|
-
return `Can overwrite, remove or end what is there, through ${
|
|
14265
|
+
return `Can overwrite, remove or end what is there, through ${listed2(destroying)}.`;
|
|
14049
14266
|
}
|
|
14050
|
-
function
|
|
14267
|
+
function listed2(items) {
|
|
14051
14268
|
const last = items.at(-1) ?? "";
|
|
14052
14269
|
return items.length === 1 ? last : `${items.slice(0, -1).join(", ")} and ${last}`;
|
|
14053
14270
|
}
|
|
@@ -14105,30 +14322,30 @@ var init_tool_reference = __esm(() => {
|
|
|
14105
14322
|
});
|
|
14106
14323
|
|
|
14107
14324
|
// src/desktop.ts
|
|
14108
|
-
import { createHash as
|
|
14325
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
14109
14326
|
import {
|
|
14110
14327
|
closeSync,
|
|
14111
|
-
existsSync as
|
|
14328
|
+
existsSync as existsSync16,
|
|
14112
14329
|
mkdtempSync as mkdtempSync2,
|
|
14113
14330
|
openSync,
|
|
14114
|
-
readFileSync as
|
|
14331
|
+
readFileSync as readFileSync13,
|
|
14115
14332
|
rmSync as rmSync7,
|
|
14116
|
-
statSync as
|
|
14333
|
+
statSync as statSync9,
|
|
14117
14334
|
writeFileSync as writeFileSync8,
|
|
14118
14335
|
writeSync
|
|
14119
14336
|
} from "node:fs";
|
|
14120
14337
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
14121
|
-
import { join as
|
|
14338
|
+
import { join as join18 } from "node:path";
|
|
14122
14339
|
import { setTimeout as delay } from "node:timers/promises";
|
|
14123
14340
|
function helperAssembly(directory = tmpdir4()) {
|
|
14124
|
-
return
|
|
14341
|
+
return join18(directory, `gdharness-desktop-${DIGEST}.dll`);
|
|
14125
14342
|
}
|
|
14126
14343
|
async function ensureHelperCompiled(directory = tmpdir4(), budgetMs = HELPER_COMPILE_MS, wait = true) {
|
|
14127
14344
|
const assembly = helperAssembly(directory);
|
|
14128
14345
|
const lock = `${assembly}.compiling`;
|
|
14129
14346
|
const until = Date.now() + budgetMs;
|
|
14130
14347
|
while (Date.now() < until) {
|
|
14131
|
-
if (
|
|
14348
|
+
if (existsSync16(assembly)) {
|
|
14132
14349
|
return null;
|
|
14133
14350
|
}
|
|
14134
14351
|
let mine = false;
|
|
@@ -14146,8 +14363,8 @@ async function ensureHelperCompiled(directory = tmpdir4(), budgetMs = HELPER_COM
|
|
|
14146
14363
|
}
|
|
14147
14364
|
}
|
|
14148
14365
|
try {
|
|
14149
|
-
const holder = Number(
|
|
14150
|
-
const old = Date.now() -
|
|
14366
|
+
const holder = Number(readFileSync13(lock, "utf8").trim());
|
|
14367
|
+
const old = Date.now() - statSync9(lock).mtimeMs > budgetMs;
|
|
14151
14368
|
if (old || Number.isInteger(holder) && holder > 0 && !alive(holder)) {
|
|
14152
14369
|
rmSync7(lock, { force: true });
|
|
14153
14370
|
continue;
|
|
@@ -14161,8 +14378,8 @@ async function ensureHelperCompiled(directory = tmpdir4(), budgetMs = HELPER_COM
|
|
|
14161
14378
|
return `the desktop helper was not compiled within ${String(budgetMs)} ms`;
|
|
14162
14379
|
}
|
|
14163
14380
|
async function compileHelper(directory, assembly, budgetMs) {
|
|
14164
|
-
const scratch = mkdtempSync2(
|
|
14165
|
-
const source =
|
|
14381
|
+
const scratch = mkdtempSync2(join18(directory, "gdharness-desktop-source-"));
|
|
14382
|
+
const source = join18(scratch, "helper.cs");
|
|
14166
14383
|
writeFileSync8(source, SOURCE, "utf8");
|
|
14167
14384
|
const literal = (path) => `'${path.replaceAll("'", "''")}'`;
|
|
14168
14385
|
const script = [
|
|
@@ -14200,7 +14417,7 @@ async function compileHelper(directory, assembly, budgetMs) {
|
|
|
14200
14417
|
});
|
|
14201
14418
|
compiler.once("close", (code) => {
|
|
14202
14419
|
clearTimeout(timer);
|
|
14203
|
-
resolve(
|
|
14420
|
+
resolve(existsSync16(assembly) ? null : `Windows PowerShell could not compile it${code === null ? " in time" : ""}: ${complained.trim() || `exit code ${String(code)}`}`);
|
|
14204
14421
|
});
|
|
14205
14422
|
});
|
|
14206
14423
|
} finally {
|
|
@@ -14425,7 +14642,317 @@ public static class GdharnessDesktop {
|
|
|
14425
14642
|
var init_desktop = __esm(() => {
|
|
14426
14643
|
init_alive();
|
|
14427
14644
|
init_spawn();
|
|
14428
|
-
DIGEST =
|
|
14645
|
+
DIGEST = createHash5("sha256").update(SOURCE).digest("hex").slice(0, 16);
|
|
14646
|
+
});
|
|
14647
|
+
|
|
14648
|
+
// src/suite-judge.ts
|
|
14649
|
+
import { readFileSync as readFileSync16 } from "node:fs";
|
|
14650
|
+
import { stripVTControlCharacters } from "node:util";
|
|
14651
|
+
function secondsOf(time) {
|
|
14652
|
+
let ms = 0;
|
|
14653
|
+
for (const [, amount, unit] of time.matchAll(/(\d+)(h|min|ms|s)\b/g)) {
|
|
14654
|
+
ms += Number(amount) * (unit === "h" ? 3600000 : unit === "min" ? 60000 : unit === "s" ? 1000 : 1);
|
|
14655
|
+
}
|
|
14656
|
+
return ms / 1000;
|
|
14657
|
+
}
|
|
14658
|
+
function funcOf(name) {
|
|
14659
|
+
const colon = name.indexOf(":");
|
|
14660
|
+
return colon < 0 ? name : name.slice(0, colon);
|
|
14661
|
+
}
|
|
14662
|
+
function readRun(printed) {
|
|
14663
|
+
const lines = stripVTControlCharacters(printed).replaceAll(`\r
|
|
14664
|
+
`, `
|
|
14665
|
+
`).split(`
|
|
14666
|
+
`);
|
|
14667
|
+
const suites = new Map;
|
|
14668
|
+
const errors = [];
|
|
14669
|
+
const leaks = [];
|
|
14670
|
+
let executed = null;
|
|
14671
|
+
let open = null;
|
|
14672
|
+
let during = null;
|
|
14673
|
+
let afterCase = [];
|
|
14674
|
+
let lastCase = null;
|
|
14675
|
+
const settleAfterCase = (owner) => {
|
|
14676
|
+
for (const held of afterCase) {
|
|
14677
|
+
errors.push({ ...held, suite: open?.path ?? null, during: owner });
|
|
14678
|
+
}
|
|
14679
|
+
afterCase = [];
|
|
14680
|
+
};
|
|
14681
|
+
for (const [at, line] of lines.entries()) {
|
|
14682
|
+
const opened = SUITE_OPENED.exec(line);
|
|
14683
|
+
if (opened?.[1] !== undefined) {
|
|
14684
|
+
settleAfterCase("after");
|
|
14685
|
+
open = suites.get(opened[1]) ?? { path: opened[1], cases: [], closed: null };
|
|
14686
|
+
suites.set(open.path, open);
|
|
14687
|
+
during = "before";
|
|
14688
|
+
lastCase = null;
|
|
14689
|
+
continue;
|
|
14690
|
+
}
|
|
14691
|
+
const caseLine = CASE_LINE.exec(line);
|
|
14692
|
+
if (caseLine !== null && open !== null) {
|
|
14693
|
+
const name = caseLine[2] ?? "";
|
|
14694
|
+
if (caseLine[3] === "STARTED") {
|
|
14695
|
+
settleAfterCase(lastCase ?? "before");
|
|
14696
|
+
during = name;
|
|
14697
|
+
} else {
|
|
14698
|
+
open.cases.push({
|
|
14699
|
+
name,
|
|
14700
|
+
func: funcOf(name),
|
|
14701
|
+
verdict: caseLine[3] ?? "",
|
|
14702
|
+
seconds: secondsOf(caseLine[4] ?? ""),
|
|
14703
|
+
at
|
|
14704
|
+
});
|
|
14705
|
+
lastCase = name;
|
|
14706
|
+
during = null;
|
|
14707
|
+
}
|
|
14708
|
+
continue;
|
|
14709
|
+
}
|
|
14710
|
+
const statistics = STATISTICS.exec(line);
|
|
14711
|
+
if (statistics !== null && open !== null) {
|
|
14712
|
+
settleAfterCase("after");
|
|
14713
|
+
open.closed = { verdict: statistics[1] ?? "", seconds: secondsOf(statistics[2] ?? "") };
|
|
14714
|
+
open = null;
|
|
14715
|
+
during = null;
|
|
14716
|
+
lastCase = null;
|
|
14717
|
+
continue;
|
|
14718
|
+
}
|
|
14719
|
+
const counted = EXECUTED.exec(line);
|
|
14720
|
+
if (counted?.[1] !== undefined) {
|
|
14721
|
+
executed = Number(counted[1]);
|
|
14722
|
+
continue;
|
|
14723
|
+
}
|
|
14724
|
+
if (LEAK.test(line)) {
|
|
14725
|
+
leaks.push(line.trim());
|
|
14726
|
+
continue;
|
|
14727
|
+
}
|
|
14728
|
+
if (suites.size === 0 && DRIVER_FALLBACK.test(line)) {
|
|
14729
|
+
errors.length = 0;
|
|
14730
|
+
continue;
|
|
14731
|
+
}
|
|
14732
|
+
const error = ERROR_LINE.exec(line);
|
|
14733
|
+
if (error === null) {
|
|
14734
|
+
continue;
|
|
14735
|
+
}
|
|
14736
|
+
const text = error[1] ?? "";
|
|
14737
|
+
if (open === null) {
|
|
14738
|
+
errors.push({ text, at, suite: null, during: null });
|
|
14739
|
+
} else if (during === null) {
|
|
14740
|
+
afterCase.push({ text, at });
|
|
14741
|
+
} else {
|
|
14742
|
+
errors.push({ text, at, suite: open.path, during });
|
|
14743
|
+
}
|
|
14744
|
+
}
|
|
14745
|
+
const last = open;
|
|
14746
|
+
const stoppedIn = last === null ? null : during === null ? `${last.path}, after ${lastCase ?? "its cases"}` : during === "before" ? `${last.path}, in before` : `${last.path} > ${during}`;
|
|
14747
|
+
settleAfterCase("after");
|
|
14748
|
+
const joined = lines.join(`
|
|
14749
|
+
`);
|
|
14750
|
+
const starved = STARVED.flatMap((pattern) => [...joined.matchAll(pattern)].map((found) => found[0]));
|
|
14751
|
+
const unloadable = scriptErrorsPrinted(lines).map((error) => `${error.path}${error.line === null ? "" : `:${String(error.line)}`}: ${error.message}`);
|
|
14752
|
+
return { lines, suites, errors, leaks, starved, executed, unloadable, stoppedIn };
|
|
14753
|
+
}
|
|
14754
|
+
function readExpected(file) {
|
|
14755
|
+
try {
|
|
14756
|
+
const parsed = JSON.parse(readFileSync16(file, "utf8"));
|
|
14757
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
14758
|
+
} catch {
|
|
14759
|
+
return {};
|
|
14760
|
+
}
|
|
14761
|
+
}
|
|
14762
|
+
function exitNamed(code) {
|
|
14763
|
+
const unsigned = code >>> 0;
|
|
14764
|
+
const status = WINDOWS_EXITS.get(unsigned);
|
|
14765
|
+
if (status !== undefined) {
|
|
14766
|
+
return `0x${unsigned.toString(16).toUpperCase()} (${status})`;
|
|
14767
|
+
}
|
|
14768
|
+
const gdunit = GDUNIT_EXITS.get(code);
|
|
14769
|
+
return gdunit === undefined ? String(code) : `${String(code)} (${gdunit})`;
|
|
14770
|
+
}
|
|
14771
|
+
function named(items) {
|
|
14772
|
+
const shown = items.slice(0, MOST_NAMED).join(", ");
|
|
14773
|
+
return items.length > MOST_NAMED ? `${shown} and ${String(items.length - MOST_NAMED)} more` : shown;
|
|
14774
|
+
}
|
|
14775
|
+
function judgeEngine(printed, handed, end) {
|
|
14776
|
+
const faults = [];
|
|
14777
|
+
const failing = new Set;
|
|
14778
|
+
const shownAt = new Set;
|
|
14779
|
+
const explained = end.code === GDUNIT_FAILED && [...printed.suites.values()].some((suite) => suite.cases.some((ran) => ran.verdict === "FAILED"));
|
|
14780
|
+
if (end.ended !== null) {
|
|
14781
|
+
faults.push(end.ended);
|
|
14782
|
+
} else if (end.code !== 0 && !explained) {
|
|
14783
|
+
faults.push(end.code === null ? `the engine was ended by ${end.signal ?? "a signal"}` : `the engine exited ${exitNamed(end.code)}`);
|
|
14784
|
+
}
|
|
14785
|
+
if (printed.unloadable.length > 0) {
|
|
14786
|
+
faults.push(`gdUnit4 could not load ${named(printed.unloadable)}, so it ran nothing`);
|
|
14787
|
+
}
|
|
14788
|
+
if (printed.executed === null || printed.executed === 0) {
|
|
14789
|
+
faults.push(printed.executed === null ? "the run never printed its summary" : "the run executed no test case at all, whatever it exited with");
|
|
14790
|
+
}
|
|
14791
|
+
for (const { path, cases } of handed.suites) {
|
|
14792
|
+
const suite = printed.suites.get(path);
|
|
14793
|
+
if (suite === undefined) {
|
|
14794
|
+
faults.push(`${path} never started`);
|
|
14795
|
+
failing.add(path);
|
|
14796
|
+
continue;
|
|
14797
|
+
}
|
|
14798
|
+
if (suite.closed === null) {
|
|
14799
|
+
faults.push(`${path} never finished`);
|
|
14800
|
+
failing.add(path);
|
|
14801
|
+
} else if (suite.closed.verdict !== "PASSED") {
|
|
14802
|
+
if (suite.cases.every((ran) => ran.verdict !== "FAILED")) {
|
|
14803
|
+
faults.push(`${path} reported ${suite.closed.verdict} with every case passing, so a hook failed`);
|
|
14804
|
+
}
|
|
14805
|
+
failing.add(path);
|
|
14806
|
+
}
|
|
14807
|
+
const finished = new Set(suite.cases.map((ran) => ran.func));
|
|
14808
|
+
const missing = cases.filter((func) => !finished.has(func));
|
|
14809
|
+
if (missing.length > 0 && suite.closed !== null) {
|
|
14810
|
+
faults.push(`${path} closed without running ${named(missing)}`);
|
|
14811
|
+
failing.add(path);
|
|
14812
|
+
}
|
|
14813
|
+
const wanted = new Set(cases);
|
|
14814
|
+
const unasked = [...finished].filter((func) => !wanted.has(func));
|
|
14815
|
+
if (unasked.length > 0) {
|
|
14816
|
+
faults.push(`${path} ran ${named(unasked)}, which gdharness did not read as cases of it to run here, so its ` + "plan of the cases was wrong");
|
|
14817
|
+
failing.add(path);
|
|
14818
|
+
}
|
|
14819
|
+
for (const ran of suite.cases) {
|
|
14820
|
+
if (ran.verdict === "FAILED" || ran.verdict === "FLAKY") {
|
|
14821
|
+
faults.push(ran.verdict === "FLAKY" ? `${path} > ${ran.name} failed before it passed on a retry` : `${path} > ${ran.name} failed`);
|
|
14822
|
+
failing.add(path);
|
|
14823
|
+
for (let at = ran.at;at < printed.lines.length && (at === ran.at || printed.lines[at]?.trim()); at += 1) {
|
|
14824
|
+
shownAt.add(at);
|
|
14825
|
+
}
|
|
14826
|
+
}
|
|
14827
|
+
}
|
|
14828
|
+
}
|
|
14829
|
+
const unexpected = unexpectedErrors(printed, handed.expected, handed.suites.map((suite) => suite.path));
|
|
14830
|
+
for (const error of unexpected.printed) {
|
|
14831
|
+
faults.push(error.suite === null ? `an engine error outside every suite: ${error.text}` : `an engine error in ${error.suite} > ${error.during ?? ""}: ${error.text}; ${declaringAdvice(error.during ?? "")}`);
|
|
14832
|
+
if (error.suite !== null) {
|
|
14833
|
+
failing.add(error.suite);
|
|
14834
|
+
}
|
|
14835
|
+
shownAt.add(error.at);
|
|
14836
|
+
shownAt.add(error.at + 1);
|
|
14837
|
+
}
|
|
14838
|
+
for (const missing of unexpected.neverPrinted) {
|
|
14839
|
+
faults.push(`${missing.suite} > ${missing.during} declares the error "${missing.text}", which it never printed`);
|
|
14840
|
+
failing.add(missing.suite);
|
|
14841
|
+
}
|
|
14842
|
+
for (const leak of printed.leaks) {
|
|
14843
|
+
faults.push(`the engine exited holding what it should have freed: ${leak}`);
|
|
14844
|
+
}
|
|
14845
|
+
const memoryExit = end.code !== null && MEMORY_EXITS.has(end.code >>> 0);
|
|
14846
|
+
const starved = memoryExit || printed.starved.length > 0;
|
|
14847
|
+
if (printed.starved.length > 0) {
|
|
14848
|
+
faults.push("the engine ran out of memory");
|
|
14849
|
+
}
|
|
14850
|
+
printed.lines.forEach((line, at) => {
|
|
14851
|
+
if (/^(?:Overall Summary|Executed test|Total execution time| ?Exit code)/.test(line.trim())) {
|
|
14852
|
+
shownAt.add(at);
|
|
14853
|
+
}
|
|
14854
|
+
});
|
|
14855
|
+
const passed = faults.length === 0;
|
|
14856
|
+
const passedSuites = handed.suites.map((suite) => suite.path).filter((path) => !failing.has(path));
|
|
14857
|
+
const cutShort = end.ended !== null || printed.executed === null || handed.suites.some(({ path }) => printed.suites.get(path)?.closed == null);
|
|
14858
|
+
return {
|
|
14859
|
+
passed,
|
|
14860
|
+
faults,
|
|
14861
|
+
passedSuites,
|
|
14862
|
+
starved,
|
|
14863
|
+
shown: passed ? [] : linesShown(printed.lines, shownAt, cutShort)
|
|
14864
|
+
};
|
|
14865
|
+
}
|
|
14866
|
+
function linesShown(lines, picked, cutShort) {
|
|
14867
|
+
const summary = [...picked].filter((at) => at < lines.length).sort((a, b) => a - b);
|
|
14868
|
+
const telling = summary.some((at) => !/^(?:Overall Summary|Executed test|Total execution|\s*Exit code)/.test(lines[at]?.trim() ?? ""));
|
|
14869
|
+
if (!telling && !cutShort) {
|
|
14870
|
+
return [];
|
|
14871
|
+
}
|
|
14872
|
+
if (!telling) {
|
|
14873
|
+
return lines.slice(Math.max(0, lines.length - TAIL)).filter((line, at, all) => at < all.length - 1 || line !== "");
|
|
14874
|
+
}
|
|
14875
|
+
const shown = [];
|
|
14876
|
+
let previous = -1;
|
|
14877
|
+
for (const at of summary) {
|
|
14878
|
+
if (previous >= 0 && at > previous + 1) {
|
|
14879
|
+
shown.push("...");
|
|
14880
|
+
}
|
|
14881
|
+
shown.push(lines[at] ?? "");
|
|
14882
|
+
previous = at;
|
|
14883
|
+
}
|
|
14884
|
+
return shown;
|
|
14885
|
+
}
|
|
14886
|
+
function declaringAdvice(during) {
|
|
14887
|
+
return `if the suite prints it on purpose, list its text under "${funcOf(during)}" in the suite's GDHARNESS_EXPECTED_ERRORS`;
|
|
14888
|
+
}
|
|
14889
|
+
function unexpectedErrors(printed, expected, paths) {
|
|
14890
|
+
const owed = new Map;
|
|
14891
|
+
for (const path of paths) {
|
|
14892
|
+
const suite = printed.suites.get(path);
|
|
14893
|
+
const declared = expected[path];
|
|
14894
|
+
if (suite === undefined || declared === undefined) {
|
|
14895
|
+
continue;
|
|
14896
|
+
}
|
|
14897
|
+
const ranHooks = suite.closed === null ? ["before"] : ["before", "after"];
|
|
14898
|
+
for (const during of [...ranHooks, ...suite.cases.map((ran) => ran.name)]) {
|
|
14899
|
+
const texts = declared[funcOf(during)];
|
|
14900
|
+
if (Array.isArray(texts) && texts.length > 0) {
|
|
14901
|
+
owed.set(`${path}
|
|
14902
|
+
${during}`, {
|
|
14903
|
+
suite: path,
|
|
14904
|
+
during,
|
|
14905
|
+
texts: texts.map((text) => String(text))
|
|
14906
|
+
});
|
|
14907
|
+
}
|
|
14908
|
+
}
|
|
14909
|
+
}
|
|
14910
|
+
const unexpected = [];
|
|
14911
|
+
for (const error of printed.errors) {
|
|
14912
|
+
const debt = error.suite === null || error.during === null ? undefined : owed.get(`${error.suite}
|
|
14913
|
+
${error.during}`);
|
|
14914
|
+
const index = debt?.texts.indexOf(error.text) ?? -1;
|
|
14915
|
+
if (debt === undefined || index < 0) {
|
|
14916
|
+
unexpected.push(error);
|
|
14917
|
+
} else {
|
|
14918
|
+
debt.texts.splice(index, 1);
|
|
14919
|
+
}
|
|
14920
|
+
}
|
|
14921
|
+
const neverPrinted = [...owed.values()].flatMap((debt) => debt.texts.map((text) => ({ suite: debt.suite, during: debt.during, text })));
|
|
14922
|
+
return { printed: unexpected, neverPrinted };
|
|
14923
|
+
}
|
|
14924
|
+
var SUITE_OPENED, TIME, CASE_LINE, STATISTICS, ERROR_LINE, EXECUTED, DRIVER_FALLBACK, LEAK, STARVED, WINDOWS_EXITS, MEMORY_EXITS, GDUNIT_FAILED = 100, GDUNIT_EXITS, TAIL = 60, MOST_NAMED = 8;
|
|
14925
|
+
var init_suite_judge = __esm(() => {
|
|
14926
|
+
init_junit();
|
|
14927
|
+
SUITE_OPENED = /^Run Test Suite:\s*(res:\/\/.+?\.gd)\s*$/;
|
|
14928
|
+
TIME = String.raw`(?:\d+h )?(?:\d+min )?(?:\d+s )?\d+ms`;
|
|
14929
|
+
CASE_LINE = new RegExp(String.raw`^\s*(res://.+?\.gd) > (.+?) (STARTED|PASSED|FAILED|FLAKY|SKIPPED|WARNING)(?: \((?:retry \d+|\d+ retries)\))?(?: (${TIME}))?\s*$`);
|
|
14930
|
+
STATISTICS = new RegExp(String.raw`^Statistics:.*\|\s*([A-Z]+)(?:\s+\(.*?\))?\s+(${TIME})\s*$`);
|
|
14931
|
+
ERROR_LINE = /^\s*(?:SCRIPT |USER )?ERROR: (.*?)\s*$/;
|
|
14932
|
+
EXECUTED = /^Executed test cases\s*:\s*\((\d+)\//;
|
|
14933
|
+
DRIVER_FALLBACK = /^\s*WARNING: Your video card drivers .*, switching to /;
|
|
14934
|
+
LEAK = /ObjectDB instances (?:were )?leaked at exit|resources? still in use at exit|Pages in use exist at exit/;
|
|
14935
|
+
STARVED = [
|
|
14936
|
+
/Parameter "mem(?:_new)?" is null\.?\s*\n\s*at: \w+ \((?:\.\/)?core\/(?:os\/memory\.cpp|templates\/cowdata\.h):/g,
|
|
14937
|
+
/ERROR: Failed decoding WebP image\.\s*\n\s*at: _webp_unpack/g
|
|
14938
|
+
];
|
|
14939
|
+
WINDOWS_EXITS = new Map([
|
|
14940
|
+
[3221225477, "STATUS_ACCESS_VIOLATION"],
|
|
14941
|
+
[3221225495, "STATUS_NO_MEMORY"],
|
|
14942
|
+
[3221225773, "STATUS_COMMITMENT_LIMIT"],
|
|
14943
|
+
[3221225794, "STATUS_DLL_INIT_FAILED"],
|
|
14944
|
+
[3221226505, "STATUS_STACK_BUFFER_OVERRUN"],
|
|
14945
|
+
[3228369022, "a module that could not be found (VcppException)"],
|
|
14946
|
+
[3228369023, "a procedure that could not be found (VcppException)"]
|
|
14947
|
+
]);
|
|
14948
|
+
MEMORY_EXITS = new Set([3221225495, 3221225773]);
|
|
14949
|
+
GDUNIT_EXITS = new Map([
|
|
14950
|
+
[GDUNIT_FAILED, "gdUnit4's code for a case that failed or errored"],
|
|
14951
|
+
[101, "gdUnit4's code for orphan nodes left behind"],
|
|
14952
|
+
[103, "gdUnit4's code for a suite that cannot run headless"],
|
|
14953
|
+
[104, "gdUnit4's code for a Godot version it does not support"],
|
|
14954
|
+
[105, "gdUnit4's code for scripts that did not compile"]
|
|
14955
|
+
]);
|
|
14429
14956
|
});
|
|
14430
14957
|
|
|
14431
14958
|
// src/whole-file.ts
|
|
@@ -14459,6 +14986,235 @@ var init_whole_file = __esm(() => {
|
|
|
14459
14986
|
HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
14460
14987
|
});
|
|
14461
14988
|
|
|
14989
|
+
// src/suite-plan.ts
|
|
14990
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync7, readFileSync as readFileSync17 } from "node:fs";
|
|
14991
|
+
import { dirname as dirname10, posix as posix3 } from "node:path";
|
|
14992
|
+
|
|
14993
|
+
class ProjectScripts {
|
|
14994
|
+
sources = new Map;
|
|
14995
|
+
classes = new Map;
|
|
14996
|
+
read = new Map;
|
|
14997
|
+
projectPath;
|
|
14998
|
+
constructor(projectPath) {
|
|
14999
|
+
this.projectPath = projectPath;
|
|
15000
|
+
eachScript(projectPath, (script, source) => {
|
|
15001
|
+
this.sources.set(script, source);
|
|
15002
|
+
const declared = declaredClassOf(source);
|
|
15003
|
+
if (declared !== null) {
|
|
15004
|
+
this.classes.set(declared, script);
|
|
15005
|
+
}
|
|
15006
|
+
});
|
|
15007
|
+
}
|
|
15008
|
+
script(path) {
|
|
15009
|
+
const known = this.read.get(path);
|
|
15010
|
+
if (known !== undefined) {
|
|
15011
|
+
return known;
|
|
15012
|
+
}
|
|
15013
|
+
const source = this.sources.get(path);
|
|
15014
|
+
if (source === undefined) {
|
|
15015
|
+
return;
|
|
15016
|
+
}
|
|
15017
|
+
const shape = shapeOf(source);
|
|
15018
|
+
const read = {
|
|
15019
|
+
source,
|
|
15020
|
+
extendsAs: shape.body.extendsAs,
|
|
15021
|
+
funcs: shape.body.declarations.filter((d) => d.kind === "func").map((d) => d.name),
|
|
15022
|
+
code: codeOf(source)
|
|
15023
|
+
};
|
|
15024
|
+
this.read.set(path, read);
|
|
15025
|
+
return read;
|
|
15026
|
+
}
|
|
15027
|
+
base(path) {
|
|
15028
|
+
const extendsAs = this.script(path)?.extendsAs ?? null;
|
|
15029
|
+
if (extendsAs === null) {
|
|
15030
|
+
return null;
|
|
15031
|
+
}
|
|
15032
|
+
const quote = extendsAs[0];
|
|
15033
|
+
if (quote === '"' || quote === "'") {
|
|
15034
|
+
const target = extendsAs.slice(1, -1);
|
|
15035
|
+
return {
|
|
15036
|
+
path: target.startsWith("res://") ? target : `res://${posix3.join(posix3.dirname(path.slice(6)), target)}`
|
|
15037
|
+
};
|
|
15038
|
+
}
|
|
15039
|
+
const name = extendsAs.split(".")[0] ?? extendsAs;
|
|
15040
|
+
const declaring = this.classes.get(name);
|
|
15041
|
+
return declaring === undefined ? { name } : { path: declaring };
|
|
15042
|
+
}
|
|
15043
|
+
suite(path) {
|
|
15044
|
+
const cases = new Set;
|
|
15045
|
+
let windowed = false;
|
|
15046
|
+
const seen = new Set;
|
|
15047
|
+
let at = path;
|
|
15048
|
+
while (!seen.has(at)) {
|
|
15049
|
+
seen.add(at);
|
|
15050
|
+
const read = this.script(at);
|
|
15051
|
+
if (read === undefined) {
|
|
15052
|
+
return null;
|
|
15053
|
+
}
|
|
15054
|
+
for (const func of read.funcs.filter((name) => name.startsWith(CASE_PREFIX))) {
|
|
15055
|
+
cases.add(func);
|
|
15056
|
+
}
|
|
15057
|
+
windowed ||= NEEDS_WINDOW.test(read.code) || SCENE_RUNNER.test(read.code);
|
|
15058
|
+
const base = this.base(at);
|
|
15059
|
+
if (base === null) {
|
|
15060
|
+
return null;
|
|
15061
|
+
}
|
|
15062
|
+
if ("name" in base ? base.name === SUITE_BASE : base.path === this.classes.get(SUITE_BASE)) {
|
|
15063
|
+
return cases.size > 0 ? { path, cases: [...cases].sort(), windowed } : null;
|
|
15064
|
+
}
|
|
15065
|
+
if ("name" in base) {
|
|
15066
|
+
return null;
|
|
15067
|
+
}
|
|
15068
|
+
at = base.path;
|
|
15069
|
+
}
|
|
15070
|
+
return null;
|
|
15071
|
+
}
|
|
15072
|
+
suitesUnder(roots) {
|
|
15073
|
+
const prefixes = roots.map((root) => root.endsWith("/") ? root : `${root}/`);
|
|
15074
|
+
return [...this.sources.keys()].filter((path) => prefixes.some((prefix) => path.startsWith(prefix))).sort().flatMap((path) => this.suite(path) ?? []);
|
|
15075
|
+
}
|
|
15076
|
+
}
|
|
15077
|
+
function readTimes(file) {
|
|
15078
|
+
try {
|
|
15079
|
+
const parsed = JSON.parse(readFileSync17(file, "utf8"));
|
|
15080
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
15081
|
+
} catch {
|
|
15082
|
+
return {};
|
|
15083
|
+
}
|
|
15084
|
+
}
|
|
15085
|
+
function writeJson(file, value) {
|
|
15086
|
+
mkdirSync7(dirname10(file), { recursive: true });
|
|
15087
|
+
writeWhole(file, `${JSON.stringify(value, null, 1)}
|
|
15088
|
+
`);
|
|
15089
|
+
}
|
|
15090
|
+
function timesAfter(times, suites, runs) {
|
|
15091
|
+
const next = { ...times };
|
|
15092
|
+
const declared = new Map(suites.map((suite) => [suite.path, suite]));
|
|
15093
|
+
const measured = new Map;
|
|
15094
|
+
for (const run of runs) {
|
|
15095
|
+
for (const printed of run.suites.values()) {
|
|
15096
|
+
if (printed.closed === null || !declared.has(printed.path)) {
|
|
15097
|
+
continue;
|
|
15098
|
+
}
|
|
15099
|
+
const entry = measured.get(printed.path) ?? { cases: new Map, around: 0 };
|
|
15100
|
+
const own = new Map;
|
|
15101
|
+
for (const ran of printed.cases) {
|
|
15102
|
+
own.set(ran.func, (own.get(ran.func) ?? 0) + ran.seconds);
|
|
15103
|
+
}
|
|
15104
|
+
const inCases = [...own.values()].reduce((sum, seconds) => sum + seconds, 0);
|
|
15105
|
+
entry.around = Math.max(entry.around, printed.closed.seconds - inCases);
|
|
15106
|
+
for (const [func, seconds] of own) {
|
|
15107
|
+
entry.cases.set(func, seconds);
|
|
15108
|
+
}
|
|
15109
|
+
measured.set(printed.path, entry);
|
|
15110
|
+
}
|
|
15111
|
+
}
|
|
15112
|
+
for (const [path, entry] of measured) {
|
|
15113
|
+
const suite = declared.get(path);
|
|
15114
|
+
const cases = {};
|
|
15115
|
+
for (const func of suite?.cases ?? []) {
|
|
15116
|
+
const seconds = entry.cases.get(func) ?? times[path]?.cases[func];
|
|
15117
|
+
if (seconds !== undefined) {
|
|
15118
|
+
cases[func] = round(seconds);
|
|
15119
|
+
}
|
|
15120
|
+
}
|
|
15121
|
+
const inCases = Object.values(cases).reduce((sum, seconds) => sum + seconds, 0);
|
|
15122
|
+
next[path] = { seconds: round(entry.around + inCases), cases };
|
|
15123
|
+
}
|
|
15124
|
+
return next;
|
|
15125
|
+
}
|
|
15126
|
+
function round(seconds) {
|
|
15127
|
+
return Math.round(seconds * 1000) / 1000;
|
|
15128
|
+
}
|
|
15129
|
+
function median(values) {
|
|
15130
|
+
if (values.length === 0) {
|
|
15131
|
+
return;
|
|
15132
|
+
}
|
|
15133
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
15134
|
+
const middle = Math.floor(sorted.length / 2);
|
|
15135
|
+
return sorted.length % 2 === 1 ? sorted[middle] : ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2;
|
|
15136
|
+
}
|
|
15137
|
+
function unitsOf(suites, times, splitPast = SPLIT_PAST_SECONDS) {
|
|
15138
|
+
const known = Object.values(times);
|
|
15139
|
+
const caseCost = median(known.flatMap((time) => Object.values(time.cases))) ?? UNTIMED_CASE;
|
|
15140
|
+
const aroundCost = median(known.map((time) => time.seconds - Object.values(time.cases).reduce((sum, s) => sum + s, 0))) ?? 0;
|
|
15141
|
+
return suites.flatMap((suite) => {
|
|
15142
|
+
const time = times[suite.path];
|
|
15143
|
+
const costOf = (func) => time?.cases[func] ?? caseCost;
|
|
15144
|
+
const around = time === undefined ? aroundCost : Math.max(0, time.seconds - Object.values(time.cases).reduce((sum, s) => sum + s, 0));
|
|
15145
|
+
const whole = around + suite.cases.reduce((sum, func) => sum + costOf(func), 0);
|
|
15146
|
+
if (suite.windowed || whole <= splitPast || suite.cases.length < 2) {
|
|
15147
|
+
return [{ suite: suite.path, cases: suite.cases, cost: whole, windowed: suite.windowed }];
|
|
15148
|
+
}
|
|
15149
|
+
return suite.cases.map((func) => ({
|
|
15150
|
+
suite: suite.path,
|
|
15151
|
+
cases: [func],
|
|
15152
|
+
cost: around + costOf(func),
|
|
15153
|
+
windowed: false
|
|
15154
|
+
}));
|
|
15155
|
+
});
|
|
15156
|
+
}
|
|
15157
|
+
function dealt(units, declared, most) {
|
|
15158
|
+
const windowed = units.filter((unit) => unit.windowed);
|
|
15159
|
+
const headless = units.filter((unit) => !unit.windowed).sort((a, b) => b.cost - a.cost || a.suite.localeCompare(b.suite) || (a.cases[0] ?? "").localeCompare(b.cases[0] ?? ""));
|
|
15160
|
+
if (units.length === 0) {
|
|
15161
|
+
return [];
|
|
15162
|
+
}
|
|
15163
|
+
const together = windowed.reduce((sum, unit) => sum + unit.cost, 0);
|
|
15164
|
+
const total = units.reduce((sum, unit) => sum + unit.cost, 0);
|
|
15165
|
+
const slowest = Math.max(together, headless[0]?.cost ?? 0);
|
|
15166
|
+
const count = Math.max(1, Math.min(most, headless.length + (windowed.length > 0 ? 1 : 0), Math.ceil(total / Math.max(slowest, ENGINE_START_SECONDS))));
|
|
15167
|
+
const bins = Array.from({ length: count }, (_, index) => index === 0 ? [...windowed] : []);
|
|
15168
|
+
const loads = Array.from({ length: count }, (_, index) => index === 0 ? together : 0);
|
|
15169
|
+
for (const unit of headless) {
|
|
15170
|
+
const lightest = loads.indexOf(Math.min(...loads));
|
|
15171
|
+
bins[lightest]?.push(unit);
|
|
15172
|
+
loads[lightest] = (loads[lightest] ?? 0) + unit.cost;
|
|
15173
|
+
}
|
|
15174
|
+
return bins.map((bin, index) => planOf(bin, declared, index === 0 && windowed.length > 0));
|
|
15175
|
+
}
|
|
15176
|
+
function windowedSaid(plans) {
|
|
15177
|
+
if (!plans.some((plan) => plan.windowed)) {
|
|
15178
|
+
return "";
|
|
15179
|
+
}
|
|
15180
|
+
return plans.length === 1 ? ", with a window" : ", one of them windowed";
|
|
15181
|
+
}
|
|
15182
|
+
function inOneEngine(units, declared) {
|
|
15183
|
+
return planOf(units, declared, units.some((unit) => unit.windowed));
|
|
15184
|
+
}
|
|
15185
|
+
function planOf(bin, declared, windowed) {
|
|
15186
|
+
const held = new Map;
|
|
15187
|
+
for (const unit of bin) {
|
|
15188
|
+
held.set(unit.suite, [...held.get(unit.suite) ?? [], ...unit.cases]);
|
|
15189
|
+
}
|
|
15190
|
+
const skipping = [...held].flatMap(([path, cases]) => (declared.get(path) ?? []).filter((func) => !cases.includes(func)).map((func) => `${path}:${func}`));
|
|
15191
|
+
return {
|
|
15192
|
+
suites: [...held].map(([path, cases]) => ({ path, cases: [...cases].sort() })),
|
|
15193
|
+
skipping,
|
|
15194
|
+
cost: bin.reduce((sum, unit) => sum + unit.cost, 0),
|
|
15195
|
+
windowed
|
|
15196
|
+
};
|
|
15197
|
+
}
|
|
15198
|
+
function viewportOf(projectPath) {
|
|
15199
|
+
const file = `${projectPath}/project.godot`;
|
|
15200
|
+
if (!existsSync18(file)) {
|
|
15201
|
+
return null;
|
|
15202
|
+
}
|
|
15203
|
+
const text = readFileSync17(file, "utf8");
|
|
15204
|
+
const width = /^window\/size\/viewport_width\s*=\s*(\d+)/m.exec(text)?.[1];
|
|
15205
|
+
const height = /^window\/size\/viewport_height\s*=\s*(\d+)/m.exec(text)?.[1];
|
|
15206
|
+
return width === undefined || height === undefined ? null : { width: Number(width), height: Number(height) };
|
|
15207
|
+
}
|
|
15208
|
+
var SUITE_BASE = "GdUnitTestSuite", CASE_PREFIX = "test_", NEEDS_WINDOW, TEST_DIRECTORIES, SCENE_RUNNER, UNTIMED_CASE = 0.02, SPLIT_PAST_SECONDS = 10, ENGINE_START_SECONDS = 5;
|
|
15209
|
+
var init_suite_plan = __esm(() => {
|
|
15210
|
+
init_class_cache();
|
|
15211
|
+
init_gdscript_source();
|
|
15212
|
+
init_whole_file();
|
|
15213
|
+
NEEDS_WINDOW = /^const\s+GDHARNESS_NEEDS_WINDOW\b[^=\n]*=\s*true\b/m;
|
|
15214
|
+
TEST_DIRECTORIES = ["tests", "test"];
|
|
15215
|
+
SCENE_RUNNER = /\bscene_runner\s*\(/;
|
|
15216
|
+
});
|
|
15217
|
+
|
|
14462
15218
|
// node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
|
|
14463
15219
|
var __create2, __defProp2, __getOwnPropDesc, __getOwnPropNames2, __getProtoOf2, __hasOwnProp2, __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports), __exportAll = (all, symbols) => {
|
|
14464
15220
|
let target = {};
|
|
@@ -36060,13 +36816,13 @@ var init_stdio = __esm(() => {
|
|
|
36060
36816
|
});
|
|
36061
36817
|
|
|
36062
36818
|
// src/autoload-failure.ts
|
|
36063
|
-
import { existsSync as
|
|
36064
|
-
import { join as
|
|
36819
|
+
import { existsSync as existsSync20, readFileSync as readFileSync20 } from "node:fs";
|
|
36820
|
+
import { join as join23 } from "node:path";
|
|
36065
36821
|
function scriptsAutoloaded(projectPath) {
|
|
36066
36822
|
const scripts = new Map;
|
|
36067
36823
|
let settings;
|
|
36068
36824
|
try {
|
|
36069
|
-
settings = parseProjectGodot(
|
|
36825
|
+
settings = parseProjectGodot(readFileSync20(join23(projectPath, "project.godot"), "utf8"));
|
|
36070
36826
|
} catch {
|
|
36071
36827
|
return scripts;
|
|
36072
36828
|
}
|
|
@@ -36075,12 +36831,12 @@ function scriptsAutoloaded(projectPath) {
|
|
|
36075
36831
|
scripts.set(`res://${path}`, name);
|
|
36076
36832
|
continue;
|
|
36077
36833
|
}
|
|
36078
|
-
const scene =
|
|
36079
|
-
if (!
|
|
36834
|
+
const scene = join23(projectPath, path);
|
|
36835
|
+
if (!existsSync20(scene)) {
|
|
36080
36836
|
continue;
|
|
36081
36837
|
}
|
|
36082
36838
|
try {
|
|
36083
|
-
for (const named of
|
|
36839
|
+
for (const named of readFileSync20(scene, "utf8").matchAll(/^\[ext_resource\b[^\]]*\btype="Script"[^\]]*\bpath="(res:\/\/[^"]+)"/gm)) {
|
|
36084
36840
|
scripts.set(named[1] ?? "", name);
|
|
36085
36841
|
}
|
|
36086
36842
|
} catch {}
|
|
@@ -36099,10 +36855,10 @@ var init_autoload_failure = __esm(() => {
|
|
|
36099
36855
|
});
|
|
36100
36856
|
|
|
36101
36857
|
// src/runtime-client.ts
|
|
36102
|
-
import { existsSync as
|
|
36858
|
+
import { existsSync as existsSync21, readdirSync as readdirSync13, readFileSync as readFileSync21, statSync as statSync13, unlinkSync as unlinkSync2 } from "node:fs";
|
|
36103
36859
|
import { createConnection } from "node:net";
|
|
36104
36860
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
36105
|
-
import { dirname as dirname12, join as
|
|
36861
|
+
import { dirname as dirname12, join as join24, resolve as resolve5 } from "node:path";
|
|
36106
36862
|
import { setTimeout as delay4 } from "node:timers/promises";
|
|
36107
36863
|
function runtimeDirectory(variables = process.env) {
|
|
36108
36864
|
const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
|
|
@@ -36110,20 +36866,20 @@ function runtimeDirectory(variables = process.env) {
|
|
|
36110
36866
|
return explicit;
|
|
36111
36867
|
}
|
|
36112
36868
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
36113
|
-
return
|
|
36869
|
+
return join24(perUser ?? tmpdir5(), "gdharness");
|
|
36114
36870
|
}
|
|
36115
36871
|
function runtimeDirectories(variables = process.env) {
|
|
36116
36872
|
const candidates = [runtimeDirectory(variables)];
|
|
36117
36873
|
const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
|
|
36118
36874
|
for (const base of fallbacks) {
|
|
36119
|
-
candidates.push(
|
|
36875
|
+
candidates.push(join24(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
|
|
36120
36876
|
}
|
|
36121
36877
|
return [...new Set(candidates.map((path) => resolve5(path)))];
|
|
36122
36878
|
}
|
|
36123
36879
|
function errorReportOf(pid, directories = runtimeDirectories()) {
|
|
36124
36880
|
for (const directory of directories) {
|
|
36125
|
-
const path =
|
|
36126
|
-
if (
|
|
36881
|
+
const path = join24(directory, `runtime-${pid}.log`);
|
|
36882
|
+
if (existsSync21(path)) {
|
|
36127
36883
|
return path;
|
|
36128
36884
|
}
|
|
36129
36885
|
}
|
|
@@ -36154,7 +36910,7 @@ function reportAlreadyInTranscript(transcript, report) {
|
|
|
36154
36910
|
function parseAnnouncement(file, pid) {
|
|
36155
36911
|
let fields;
|
|
36156
36912
|
try {
|
|
36157
|
-
fields = asParams(JSON.parse(
|
|
36913
|
+
fields = asParams(JSON.parse(readFileSync21(file, "utf8")));
|
|
36158
36914
|
} catch {
|
|
36159
36915
|
return { kind: "rubbish" };
|
|
36160
36916
|
}
|
|
@@ -36171,7 +36927,7 @@ function parseAnnouncement(file, pid) {
|
|
|
36171
36927
|
};
|
|
36172
36928
|
let announcedAt;
|
|
36173
36929
|
try {
|
|
36174
|
-
announcedAt =
|
|
36930
|
+
announcedAt = statSync13(file).mtimeMs;
|
|
36175
36931
|
} catch {}
|
|
36176
36932
|
const when = announcedAt === undefined ? {} : { announcedAt };
|
|
36177
36933
|
if (protocol !== RUNTIME_PROTOCOL) {
|
|
@@ -36224,7 +36980,7 @@ function discoverRuntimes(directories = runtimeDirectories()) {
|
|
|
36224
36980
|
function wentAway(file, pid) {
|
|
36225
36981
|
let project = "";
|
|
36226
36982
|
try {
|
|
36227
|
-
const fields = asParams(JSON.parse(
|
|
36983
|
+
const fields = asParams(JSON.parse(readFileSync21(file, "utf8")));
|
|
36228
36984
|
project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
|
|
36229
36985
|
} catch {
|
|
36230
36986
|
return;
|
|
@@ -36267,29 +37023,29 @@ function strangersAmong(candidates, now, startTimes = startTimesOf) {
|
|
|
36267
37023
|
}).map((one) => one.file));
|
|
36268
37024
|
}
|
|
36269
37025
|
function announcedIn(directory) {
|
|
36270
|
-
if (!
|
|
37026
|
+
if (!existsSync21(directory)) {
|
|
36271
37027
|
return [];
|
|
36272
37028
|
}
|
|
36273
37029
|
const found = [];
|
|
36274
37030
|
const candidates = [];
|
|
36275
37031
|
const entries = [];
|
|
36276
|
-
for (const entry of
|
|
37032
|
+
for (const entry of readdirSync13(directory)) {
|
|
36277
37033
|
const report = ERROR_REPORT_PATTERN.exec(entry);
|
|
36278
37034
|
if (report) {
|
|
36279
|
-
sweepErrorReport(
|
|
37035
|
+
sweepErrorReport(join24(directory, entry), Number.parseInt(report[1] ?? "", 10));
|
|
36280
37036
|
continue;
|
|
36281
37037
|
}
|
|
36282
37038
|
const match = ANNOUNCEMENT_PATTERN.exec(entry);
|
|
36283
37039
|
if (!match) {
|
|
36284
37040
|
continue;
|
|
36285
37041
|
}
|
|
36286
|
-
const file =
|
|
37042
|
+
const file = join24(directory, entry);
|
|
36287
37043
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
36288
37044
|
const running = alive(pid);
|
|
36289
37045
|
entries.push({ file, pid, running });
|
|
36290
37046
|
if (running) {
|
|
36291
37047
|
try {
|
|
36292
|
-
candidates.push({ file, pid, writtenAt:
|
|
37048
|
+
candidates.push({ file, pid, writtenAt: statSync13(file).mtimeMs });
|
|
36293
37049
|
} catch {}
|
|
36294
37050
|
}
|
|
36295
37051
|
}
|
|
@@ -36311,7 +37067,7 @@ function announcedIn(directory) {
|
|
|
36311
37067
|
found.push(announced);
|
|
36312
37068
|
}
|
|
36313
37069
|
const held = new Set(entries.map((one) => one.file));
|
|
36314
|
-
const here =
|
|
37070
|
+
const here = join24(directory, ".");
|
|
36315
37071
|
for (const file of verdicts.keys()) {
|
|
36316
37072
|
if (dirname12(file) === here && !held.has(file)) {
|
|
36317
37073
|
verdicts.delete(file);
|
|
@@ -36320,7 +37076,7 @@ function announcedIn(directory) {
|
|
|
36320
37076
|
return found;
|
|
36321
37077
|
}
|
|
36322
37078
|
async function announcementEnded(pid, directories = runtimeDirectories()) {
|
|
36323
|
-
const files = directories.map((directory) =>
|
|
37079
|
+
const files = directories.map((directory) => join24(directory, `runtime-${pid}.json`)).filter((file) => existsSync21(file));
|
|
36324
37080
|
const first = files[0];
|
|
36325
37081
|
if (first === undefined) {
|
|
36326
37082
|
return;
|
|
@@ -36346,7 +37102,7 @@ function sweepErrorReport(path, pid, now = Date.now()) {
|
|
|
36346
37102
|
return;
|
|
36347
37103
|
}
|
|
36348
37104
|
try {
|
|
36349
|
-
if (now -
|
|
37105
|
+
if (now - statSync13(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
|
|
36350
37106
|
unlinkSync2(path);
|
|
36351
37107
|
}
|
|
36352
37108
|
} catch {}
|
|
@@ -36739,14 +37495,14 @@ var init_runtime_client = __esm(() => {
|
|
|
36739
37495
|
});
|
|
36740
37496
|
|
|
36741
37497
|
// src/boot-note.ts
|
|
36742
|
-
import { mkdirSync as mkdirSync9, readFileSync as
|
|
36743
|
-
import { dirname as dirname13, join as
|
|
37498
|
+
import { mkdirSync as mkdirSync9, readFileSync as readFileSync22, writeFileSync as writeFileSync11 } from "node:fs";
|
|
37499
|
+
import { dirname as dirname13, join as join25 } from "node:path";
|
|
36744
37500
|
function bootNotePath(projectPath) {
|
|
36745
|
-
return
|
|
37501
|
+
return join25(projectPath, ".godot", "gdharness-boot.json");
|
|
36746
37502
|
}
|
|
36747
37503
|
function readScenes(projectPath) {
|
|
36748
37504
|
try {
|
|
36749
|
-
return asParams(asParams(JSON.parse(
|
|
37505
|
+
return asParams(asParams(JSON.parse(readFileSync22(bootNotePath(projectPath), "utf8")))["scenes"]);
|
|
36750
37506
|
} catch {
|
|
36751
37507
|
return {};
|
|
36752
37508
|
}
|
|
@@ -36788,10 +37544,10 @@ var init_boot_note = __esm(() => {
|
|
|
36788
37544
|
});
|
|
36789
37545
|
|
|
36790
37546
|
// src/breakpoint-note.ts
|
|
36791
|
-
import { mkdirSync as mkdirSync10, readFileSync as
|
|
36792
|
-
import { dirname as dirname14, isAbsolute as isAbsolute5, join as
|
|
37547
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync23, rmSync as rmSync11, writeFileSync as writeFileSync12 } from "node:fs";
|
|
37548
|
+
import { dirname as dirname14, isAbsolute as isAbsolute5, join as join26, relative as relative4 } from "node:path";
|
|
36793
37549
|
function breakpointNotePath(projectPath) {
|
|
36794
|
-
return
|
|
37550
|
+
return join26(projectPath, ".godot", "gdharness-breakpoints.json");
|
|
36795
37551
|
}
|
|
36796
37552
|
function writeBreakpointNote(projectPath, held) {
|
|
36797
37553
|
const path = breakpointNotePath(projectPath);
|
|
@@ -36817,7 +37573,7 @@ function writeBreakpointNote(projectPath, held) {
|
|
|
36817
37573
|
}
|
|
36818
37574
|
function readBreakpointNote(projectPath) {
|
|
36819
37575
|
try {
|
|
36820
|
-
const parsed = JSON.parse(
|
|
37576
|
+
const parsed = JSON.parse(readFileSync23(breakpointNotePath(projectPath), "utf8"));
|
|
36821
37577
|
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.breakpoints)) {
|
|
36822
37578
|
return [];
|
|
36823
37579
|
}
|
|
@@ -36832,7 +37588,7 @@ function readBreakpointNote(projectPath) {
|
|
|
36832
37588
|
}
|
|
36833
37589
|
const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
|
|
36834
37590
|
if (lines.length > 0) {
|
|
36835
|
-
held.push({ scriptPath: realAsFarAsItExists(
|
|
37591
|
+
held.push({ scriptPath: realAsFarAsItExists(join26(projectPath, script)), lines });
|
|
36836
37592
|
}
|
|
36837
37593
|
}
|
|
36838
37594
|
return held;
|
|
@@ -36850,14 +37606,14 @@ var init_breakpoint_note = __esm(() => {
|
|
|
36850
37606
|
});
|
|
36851
37607
|
|
|
36852
37608
|
// src/class-note.ts
|
|
36853
|
-
import { mkdirSync as mkdirSync11, readFileSync as
|
|
36854
|
-
import { dirname as dirname15, join as
|
|
37609
|
+
import { mkdirSync as mkdirSync11, readFileSync as readFileSync24 } from "node:fs";
|
|
37610
|
+
import { dirname as dirname15, join as join27 } from "node:path";
|
|
36855
37611
|
function classNotePath(projectPath) {
|
|
36856
|
-
return
|
|
37612
|
+
return join27(projectPath, ".godot", "gdharness-classes.json");
|
|
36857
37613
|
}
|
|
36858
37614
|
function readClassNote(projectPath) {
|
|
36859
37615
|
try {
|
|
36860
|
-
const fields = asParams(JSON.parse(
|
|
37616
|
+
const fields = asParams(JSON.parse(readFileSync24(classNotePath(projectPath), "utf8")));
|
|
36861
37617
|
const rebuiltWith = readArray(fields, "rebuiltWith");
|
|
36862
37618
|
if (rebuiltWith === undefined || !rebuiltWith.every((name) => typeof name === "string")) {
|
|
36863
37619
|
return null;
|
|
@@ -36899,16 +37655,16 @@ var init_class_note = __esm(() => {
|
|
|
36899
37655
|
});
|
|
36900
37656
|
|
|
36901
37657
|
// src/config-pin.ts
|
|
36902
|
-
import { existsSync as
|
|
36903
|
-
import { join as
|
|
37658
|
+
import { existsSync as existsSync22, readFileSync as readFileSync25 } from "node:fs";
|
|
37659
|
+
import { join as join28 } from "node:path";
|
|
36904
37660
|
function versionsNamedInConfig(projectPath) {
|
|
36905
|
-
const config =
|
|
36906
|
-
if (!
|
|
37661
|
+
const config = join28(projectPath, ".mcp.json");
|
|
37662
|
+
if (!existsSync22(config)) {
|
|
36907
37663
|
return [];
|
|
36908
37664
|
}
|
|
36909
37665
|
let text;
|
|
36910
37666
|
try {
|
|
36911
|
-
text =
|
|
37667
|
+
text = readFileSync25(config, "utf8");
|
|
36912
37668
|
JSON.parse(text);
|
|
36913
37669
|
} catch {
|
|
36914
37670
|
return [];
|
|
@@ -36929,10 +37685,10 @@ var init_config_pin = __esm(() => {
|
|
|
36929
37685
|
});
|
|
36930
37686
|
|
|
36931
37687
|
// src/cut-test-run.ts
|
|
36932
|
-
import { existsSync as
|
|
36933
|
-
import { dirname as dirname16, join as
|
|
37688
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as readFileSync26, rmSync as rmSync12, writeFileSync as writeFileSync13 } from "node:fs";
|
|
37689
|
+
import { dirname as dirname16, join as join29 } from "node:path";
|
|
36934
37690
|
function keptAt(projectPath) {
|
|
36935
|
-
return
|
|
37691
|
+
return join29(projectPath, ".godot", "gdharness-reports", "cut-run.json");
|
|
36936
37692
|
}
|
|
36937
37693
|
function keepCutTestRun(projectPath, run) {
|
|
36938
37694
|
const path = keptAt(projectPath);
|
|
@@ -36944,11 +37700,11 @@ function keepCutTestRun(projectPath, run) {
|
|
|
36944
37700
|
}
|
|
36945
37701
|
function takeCutTestRun(projectPath) {
|
|
36946
37702
|
const path = keptAt(projectPath);
|
|
36947
|
-
if (!
|
|
37703
|
+
if (!existsSync23(path)) {
|
|
36948
37704
|
return null;
|
|
36949
37705
|
}
|
|
36950
37706
|
try {
|
|
36951
|
-
const run = JSON.parse(
|
|
37707
|
+
const run = JSON.parse(readFileSync26(path, "utf8"));
|
|
36952
37708
|
rmSync12(path, { force: true });
|
|
36953
37709
|
return typeof run.cutAt === "string" && Array.isArray(run.paths) && run.paths.every((path) => typeof path === "string") ? run : null;
|
|
36954
37710
|
} catch {
|
|
@@ -37761,25 +38517,25 @@ var init_dap_client = __esm(() => {
|
|
|
37761
38517
|
// src/editor-log.ts
|
|
37762
38518
|
import {
|
|
37763
38519
|
closeSync as closeSync4,
|
|
37764
|
-
existsSync as
|
|
38520
|
+
existsSync as existsSync24,
|
|
37765
38521
|
mkdirSync as mkdirSync13,
|
|
37766
38522
|
openSync as openSync4,
|
|
37767
|
-
readFileSync as
|
|
38523
|
+
readFileSync as readFileSync27,
|
|
37768
38524
|
readSync as readSync2,
|
|
37769
38525
|
rmSync as rmSync13,
|
|
37770
|
-
statSync as
|
|
38526
|
+
statSync as statSync14,
|
|
37771
38527
|
writeFileSync as writeFileSync14
|
|
37772
38528
|
} from "node:fs";
|
|
37773
|
-
import { dirname as dirname17, join as
|
|
38529
|
+
import { dirname as dirname17, join as join30 } from "node:path";
|
|
37774
38530
|
function editorLogPath(projectPath) {
|
|
37775
|
-
return
|
|
38531
|
+
return join30(projectPath, ".godot", "gdharness-editor.log");
|
|
37776
38532
|
}
|
|
37777
38533
|
function editorLogNotePath(projectPath) {
|
|
37778
|
-
return
|
|
38534
|
+
return join30(projectPath, ".godot", "gdharness-editor.json");
|
|
37779
38535
|
}
|
|
37780
38536
|
function readEditorLogNote(projectPath) {
|
|
37781
38537
|
try {
|
|
37782
|
-
const fields = asParams(JSON.parse(
|
|
38538
|
+
const fields = asParams(JSON.parse(readFileSync27(editorLogNotePath(projectPath), "utf8")));
|
|
37783
38539
|
const pid = readNumber(fields, "pid");
|
|
37784
38540
|
const startedAt = readString(fields, "startedAt");
|
|
37785
38541
|
return pid !== undefined && Number.isInteger(pid) && pid > 0 && startedAt !== undefined ? { pid, startedAt } : null;
|
|
@@ -37820,7 +38576,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
|
|
|
37820
38576
|
return { kind: "another editor", notedPid: noted?.pid ?? null };
|
|
37821
38577
|
}
|
|
37822
38578
|
const path = editorLogPath(projectPath);
|
|
37823
|
-
if (!
|
|
38579
|
+
if (!existsSync24(path)) {
|
|
37824
38580
|
return { kind: "not written" };
|
|
37825
38581
|
}
|
|
37826
38582
|
const log = new GameLog;
|
|
@@ -37876,9 +38632,9 @@ function foldProgress(text) {
|
|
|
37876
38632
|
`);
|
|
37877
38633
|
}
|
|
37878
38634
|
function theFirstOf(path) {
|
|
37879
|
-
const whole =
|
|
38635
|
+
const whole = statSync14(path).size;
|
|
37880
38636
|
if (whole <= MOST_CONSOLE_BYTES) {
|
|
37881
|
-
return { text:
|
|
38637
|
+
return { text: readFileSync27(path, "utf8") };
|
|
37882
38638
|
}
|
|
37883
38639
|
const handle = openSync4(path, "r");
|
|
37884
38640
|
try {
|
|
@@ -38039,7 +38795,7 @@ var init_editor_log = __esm(() => {
|
|
|
38039
38795
|
});
|
|
38040
38796
|
|
|
38041
38797
|
// src/rename.ts
|
|
38042
|
-
import { existsSync as
|
|
38798
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync14, readFileSync as readFileSync28, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync15 } from "node:fs";
|
|
38043
38799
|
import { dirname as dirname18, posix as posix4 } from "node:path";
|
|
38044
38800
|
|
|
38045
38801
|
class Script {
|
|
@@ -38744,11 +39500,11 @@ function rewritesOf(plan, texts, move) {
|
|
|
38744
39500
|
}
|
|
38745
39501
|
function applyRewrites(rewrites) {
|
|
38746
39502
|
for (const rewrite of rewrites) {
|
|
38747
|
-
const now =
|
|
39503
|
+
const now = readFileSync28(rewrite.absolute, "utf8");
|
|
38748
39504
|
if (now !== rewrite.before) {
|
|
38749
39505
|
throw new Error(`${rewrite.path} changed on disk while the rename was being worked out; nothing was written`);
|
|
38750
39506
|
}
|
|
38751
|
-
if (rewrite.writeTo !== rewrite.absolute &&
|
|
39507
|
+
if (rewrite.writeTo !== rewrite.absolute && existsSync25(rewrite.writeTo)) {
|
|
38752
39508
|
throw new Error(`${rewrite.writeTo} already exists; nothing was written`);
|
|
38753
39509
|
}
|
|
38754
39510
|
}
|
|
@@ -38762,7 +39518,7 @@ function applyRewrites(rewrites) {
|
|
|
38762
39518
|
let movedUid = false;
|
|
38763
39519
|
if (rewrite.writeTo !== rewrite.absolute) {
|
|
38764
39520
|
unlinkSync3(rewrite.absolute);
|
|
38765
|
-
if (
|
|
39521
|
+
if (existsSync25(`${rewrite.absolute}.uid`)) {
|
|
38766
39522
|
renameSync3(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
|
|
38767
39523
|
movedUid = true;
|
|
38768
39524
|
}
|
|
@@ -38808,10 +39564,10 @@ var init_rename = __esm(() => {
|
|
|
38808
39564
|
});
|
|
38809
39565
|
|
|
38810
39566
|
// src/lsp_client.ts
|
|
38811
|
-
import { existsSync as
|
|
39567
|
+
import { existsSync as existsSync26, realpathSync as realpathSync3 } from "node:fs";
|
|
38812
39568
|
import { readFile, realpath } from "node:fs/promises";
|
|
38813
39569
|
import { createConnection as createConnection3 } from "node:net";
|
|
38814
|
-
import { dirname as dirname19, join as
|
|
39570
|
+
import { dirname as dirname19, join as join31, resolve as resolve6 } from "node:path";
|
|
38815
39571
|
import { setTimeout as delay6 } from "node:timers/promises";
|
|
38816
39572
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
38817
39573
|
function initializeTimeoutMs() {
|
|
@@ -39489,7 +40245,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
|
|
|
39489
40245
|
} catch {
|
|
39490
40246
|
throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
|
|
39491
40247
|
}
|
|
39492
|
-
if (!
|
|
40248
|
+
if (!existsSync26(join31(projectPath, "project.godot"))) {
|
|
39493
40249
|
throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
|
|
39494
40250
|
}
|
|
39495
40251
|
const contained = resolveWithinProject(projectPath, scriptPathValue);
|
|
@@ -39649,8 +40405,8 @@ var init_lsp_client = __esm(() => {
|
|
|
39649
40405
|
});
|
|
39650
40406
|
|
|
39651
40407
|
// src/rename-symbol.ts
|
|
39652
|
-
import { existsSync as
|
|
39653
|
-
import { basename as basename8, dirname as dirname20, join as
|
|
40408
|
+
import { existsSync as existsSync27, mkdirSync as mkdirSync15, readdirSync as readdirSync14, realpathSync as realpathSync4, rmSync as rmSync14, writeFileSync as writeFileSync16 } from "node:fs";
|
|
40409
|
+
import { basename as basename8, dirname as dirname20, join as join32, relative as relative5, resolve as resolve7 } from "node:path";
|
|
39654
40410
|
function resourcePathOf(projectPath, absolute) {
|
|
39655
40411
|
const inside = relative5(onDiskSpelling(projectPath), onDiskSpelling(absolute));
|
|
39656
40412
|
if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
|
|
@@ -39663,7 +40419,7 @@ function onDiskSpelling(path) {
|
|
|
39663
40419
|
let at = resolve7(path);
|
|
39664
40420
|
for (;; ) {
|
|
39665
40421
|
try {
|
|
39666
|
-
return
|
|
40422
|
+
return join32(realpathSync4.native(at), ...missing.reverse());
|
|
39667
40423
|
} catch {
|
|
39668
40424
|
const parent = dirname20(at);
|
|
39669
40425
|
if (parent === at) {
|
|
@@ -39717,7 +40473,7 @@ async function renameSymbol(request, services) {
|
|
|
39717
40473
|
reason: `newScriptPath must be a .gd path inside the project, and ${request.newScriptPath} is not.`
|
|
39718
40474
|
};
|
|
39719
40475
|
}
|
|
39720
|
-
if (
|
|
40476
|
+
if (existsSync27(request.newScriptPath)) {
|
|
39721
40477
|
return { ok: false, reason: `${moveTo} already exists, so the script cannot be moved there.` };
|
|
39722
40478
|
}
|
|
39723
40479
|
}
|
|
@@ -39893,7 +40649,7 @@ async function renameSymbol(request, services) {
|
|
|
39893
40649
|
scripts,
|
|
39894
40650
|
moves: new Map([[declaringPath, moveTo]]),
|
|
39895
40651
|
named: [declaringPath],
|
|
39896
|
-
exists: (path) =>
|
|
40652
|
+
exists: (path) => existsSync27(`${projectPath}/${path.slice("res://".length)}`)
|
|
39897
40653
|
}, plan);
|
|
39898
40654
|
}
|
|
39899
40655
|
plan = mentionIgnored(plan, ignoredTexts(projectPath), symbol, moveTo === null ? [] : [declaringPath]);
|
|
@@ -40092,15 +40848,15 @@ ${mention.kind}`;
|
|
|
40092
40848
|
};
|
|
40093
40849
|
}
|
|
40094
40850
|
function writeLeftAloneDetail(projectPath, symbol, detail) {
|
|
40095
|
-
const directory =
|
|
40851
|
+
const directory = join32(projectPath, ".godot", "gdharness-renames");
|
|
40096
40852
|
mkdirSync15(directory, { recursive: true });
|
|
40097
40853
|
detailWrites += 1;
|
|
40098
|
-
const path =
|
|
40854
|
+
const path = join32(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
|
|
40099
40855
|
writeFileSync16(path, `${JSON.stringify(detail, null, 2)}
|
|
40100
40856
|
`, "utf8");
|
|
40101
|
-
const kept =
|
|
40857
|
+
const kept = readdirSync14(directory).filter((name) => name.endsWith(".json")).sort().reverse();
|
|
40102
40858
|
for (const old of kept.slice(10)) {
|
|
40103
|
-
rmSync14(
|
|
40859
|
+
rmSync14(join32(directory, old), { force: true });
|
|
40104
40860
|
}
|
|
40105
40861
|
return path;
|
|
40106
40862
|
}
|
|
@@ -40141,25 +40897,25 @@ var init_rename_symbol = __esm(() => {
|
|
|
40141
40897
|
|
|
40142
40898
|
// src/file-move.ts
|
|
40143
40899
|
import {
|
|
40144
|
-
existsSync as
|
|
40900
|
+
existsSync as existsSync28,
|
|
40145
40901
|
mkdirSync as mkdirSync16,
|
|
40146
|
-
readdirSync as
|
|
40147
|
-
readFileSync as
|
|
40902
|
+
readdirSync as readdirSync15,
|
|
40903
|
+
readFileSync as readFileSync29,
|
|
40148
40904
|
renameSync as renameSync4,
|
|
40149
|
-
statSync as
|
|
40905
|
+
statSync as statSync15,
|
|
40150
40906
|
writeFileSync as writeFileSync17
|
|
40151
40907
|
} from "node:fs";
|
|
40152
|
-
import { basename as basename9, dirname as dirname21, extname as extname3, join as
|
|
40908
|
+
import { basename as basename9, dirname as dirname21, extname as extname3, join as join33, relative as relative6 } from "node:path";
|
|
40153
40909
|
function filesUnder(directory) {
|
|
40154
|
-
return
|
|
40155
|
-
const path =
|
|
40910
|
+
return readdirSync15(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
40911
|
+
const path = join33(directory, entry.name);
|
|
40156
40912
|
return entry.isDirectory() ? filesUnder(path) : [path];
|
|
40157
40913
|
});
|
|
40158
40914
|
}
|
|
40159
40915
|
function sameFile(one, other) {
|
|
40160
40916
|
try {
|
|
40161
|
-
const a =
|
|
40162
|
-
const b =
|
|
40917
|
+
const a = statSync15(one);
|
|
40918
|
+
const b = statSync15(other);
|
|
40163
40919
|
return a.ino === b.ino && a.dev === b.dev;
|
|
40164
40920
|
} catch {
|
|
40165
40921
|
return false;
|
|
@@ -40173,8 +40929,8 @@ function unseenByTheEngine(projectPath, path) {
|
|
|
40173
40929
|
return `${path} is under ${step}, and the engine passes over every directory whose name starts with a dot`;
|
|
40174
40930
|
}
|
|
40175
40931
|
if (at < steps.length - 1) {
|
|
40176
|
-
directory =
|
|
40177
|
-
if (
|
|
40932
|
+
directory = join33(directory, step);
|
|
40933
|
+
if (existsSync28(join33(directory, ".gdignore"))) {
|
|
40178
40934
|
return `${path} is under a directory holding a .gdignore, which the engine does not load`;
|
|
40179
40935
|
}
|
|
40180
40936
|
}
|
|
@@ -40186,21 +40942,21 @@ function installedAddonOf(projectPath, path) {
|
|
|
40186
40942
|
if (steps[0] !== "addons" || steps[1] === undefined) {
|
|
40187
40943
|
return null;
|
|
40188
40944
|
}
|
|
40189
|
-
return
|
|
40945
|
+
return existsSync28(join33(projectPath, "addons", steps[1], VERSION_MARKER)) ? `res://addons/${steps[1]}` : null;
|
|
40190
40946
|
}
|
|
40191
40947
|
function binaryResources(projectPath) {
|
|
40192
40948
|
const found = [];
|
|
40193
40949
|
const visit = (directory, prefix) => {
|
|
40194
|
-
if (
|
|
40950
|
+
if (existsSync28(join33(directory, ".gdignore"))) {
|
|
40195
40951
|
return;
|
|
40196
40952
|
}
|
|
40197
|
-
for (const entry of
|
|
40953
|
+
for (const entry of readdirSync15(directory, { withFileTypes: true })) {
|
|
40198
40954
|
if (entry.name.startsWith(".")) {
|
|
40199
40955
|
continue;
|
|
40200
40956
|
}
|
|
40201
40957
|
const path = `${prefix}${entry.name}`;
|
|
40202
40958
|
if (entry.isDirectory()) {
|
|
40203
|
-
visit(
|
|
40959
|
+
visit(join33(directory, entry.name), `${path}/`);
|
|
40204
40960
|
} else if (/\.(scn|res)$/i.test(entry.name)) {
|
|
40205
40961
|
found.push(path);
|
|
40206
40962
|
}
|
|
@@ -40210,7 +40966,7 @@ function binaryResources(projectPath) {
|
|
|
40210
40966
|
return found;
|
|
40211
40967
|
}
|
|
40212
40968
|
function targetPathOf(projectPath, absolute) {
|
|
40213
|
-
const inside = relative6(onDiskSpelling(projectPath),
|
|
40969
|
+
const inside = relative6(onDiskSpelling(projectPath), join33(onDiskSpelling(dirname21(absolute)), basename9(absolute)));
|
|
40214
40970
|
if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
|
|
40215
40971
|
return null;
|
|
40216
40972
|
}
|
|
@@ -40228,13 +40984,13 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
|
|
|
40228
40984
|
if (to === null) {
|
|
40229
40985
|
return { ok: false, reason: `newPath must be a path inside the project, and ${absoluteTo} is not.` };
|
|
40230
40986
|
}
|
|
40231
|
-
if (!
|
|
40987
|
+
if (!existsSync28(absoluteFrom)) {
|
|
40232
40988
|
return { ok: false, reason: `There is nothing at ${from} to move.` };
|
|
40233
40989
|
}
|
|
40234
40990
|
if (from === to) {
|
|
40235
40991
|
return { ok: false, reason: `${from} is already there; nothing to move.` };
|
|
40236
40992
|
}
|
|
40237
|
-
const isDirectory =
|
|
40993
|
+
const isDirectory = statSync15(absoluteFrom).isDirectory();
|
|
40238
40994
|
if (from === "res://project.godot") {
|
|
40239
40995
|
return {
|
|
40240
40996
|
ok: false,
|
|
@@ -40257,7 +41013,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
|
|
|
40257
41013
|
if (isDirectory && to.startsWith(`${from}/`)) {
|
|
40258
41014
|
return { ok: false, reason: `${from} cannot be moved into itself.` };
|
|
40259
41015
|
}
|
|
40260
|
-
if (
|
|
41016
|
+
if (existsSync28(absoluteTo) && !sameFile(absoluteFrom, absoluteTo)) {
|
|
40261
41017
|
return { ok: false, reason: `${to} already exists, so nothing was moved there.` };
|
|
40262
41018
|
}
|
|
40263
41019
|
const unseen = unseenByTheEngine(projectPath, to);
|
|
@@ -40265,7 +41021,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
|
|
|
40265
41021
|
return { ok: false, reason: `${unseen}, so nothing there could load what was moved.` };
|
|
40266
41022
|
}
|
|
40267
41023
|
if (!isDirectory) {
|
|
40268
|
-
const owner = SIDECARS.map((sidecar) => from.endsWith(sidecar) ? from.slice(0, -sidecar.length) : null).find((path) => path !== null &&
|
|
41024
|
+
const owner = SIDECARS.map((sidecar) => from.endsWith(sidecar) ? from.slice(0, -sidecar.length) : null).find((path) => path !== null && existsSync28(join33(projectPath, path.slice("res://".length))));
|
|
40269
41025
|
if (owner !== undefined && owner !== null) {
|
|
40270
41026
|
return { ok: false, reason: `${from} belongs to ${owner} and moves with it: move ${owner} instead.` };
|
|
40271
41027
|
}
|
|
@@ -40287,7 +41043,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
|
|
|
40287
41043
|
} else {
|
|
40288
41044
|
moves.set(from, to);
|
|
40289
41045
|
for (const sidecar of SIDECARS) {
|
|
40290
|
-
if (
|
|
41046
|
+
if (existsSync28(`${absoluteFrom}${sidecar}`)) {
|
|
40291
41047
|
moves.set(`${from}${sidecar}`, `${to}${sidecar}`);
|
|
40292
41048
|
}
|
|
40293
41049
|
}
|
|
@@ -40299,12 +41055,12 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
|
|
|
40299
41055
|
scripts,
|
|
40300
41056
|
moves,
|
|
40301
41057
|
named: [from],
|
|
40302
|
-
exists: (path) =>
|
|
41058
|
+
exists: (path) => existsSync28(join33(projectPath, path.slice("res://".length)))
|
|
40303
41059
|
}, { edits: [], mentions: [] });
|
|
40304
41060
|
plan = mentionIgnored(plan, ignoredTexts(projectPath), null, [from]);
|
|
40305
41061
|
const binaries = [];
|
|
40306
41062
|
for (const path of binaryResources(projectPath)) {
|
|
40307
|
-
const bytes =
|
|
41063
|
+
const bytes = readFileSync29(join33(projectPath, path.slice("res://".length)));
|
|
40308
41064
|
const names = [...moves.keys()].find((moving) => bytes.includes(moving));
|
|
40309
41065
|
if (names !== undefined) {
|
|
40310
41066
|
binaries.push({
|
|
@@ -40323,7 +41079,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
|
|
|
40323
41079
|
}));
|
|
40324
41080
|
const left = leftAloneReport(mentions);
|
|
40325
41081
|
const rewrites = rewritesOf(plan, texts, null);
|
|
40326
|
-
const absoluteOf = (path) =>
|
|
41082
|
+
const absoluteOf = (path) => join33(projectPath, path.slice("res://".length));
|
|
40327
41083
|
const rewritten = new Map(rewrites.map((rewrite) => [rewrite.path, rewrite]));
|
|
40328
41084
|
const moved = [...moves].map(([source, target]) => {
|
|
40329
41085
|
const rewrite = rewritten.get(source);
|
|
@@ -40365,18 +41121,18 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
|
|
|
40365
41121
|
}
|
|
40366
41122
|
function applyFileMove(outcome) {
|
|
40367
41123
|
for (const rewrite of outcome.inPlace) {
|
|
40368
|
-
if (
|
|
41124
|
+
if (readFileSync29(rewrite.absolute, "utf8") !== rewrite.before) {
|
|
40369
41125
|
throw new Error(`${rewrite.path} changed on disk while the move was being worked out; nothing was written`);
|
|
40370
41126
|
}
|
|
40371
41127
|
}
|
|
40372
41128
|
for (const file of outcome.moved) {
|
|
40373
|
-
if (!
|
|
41129
|
+
if (!existsSync28(file.absoluteFrom)) {
|
|
40374
41130
|
throw new Error(`${file.from} is gone from disk; nothing was written`);
|
|
40375
41131
|
}
|
|
40376
|
-
if (file.text !== null &&
|
|
41132
|
+
if (file.text !== null && readFileSync29(file.absoluteFrom, "utf8") !== file.text.before) {
|
|
40377
41133
|
throw new Error(`${file.from} changed on disk while the move was being worked out; nothing was written`);
|
|
40378
41134
|
}
|
|
40379
|
-
if (
|
|
41135
|
+
if (existsSync28(file.absoluteTo) && !sameFile(file.absoluteFrom, file.absoluteTo)) {
|
|
40380
41136
|
throw new Error(`${file.to} already exists; nothing was written`);
|
|
40381
41137
|
}
|
|
40382
41138
|
}
|
|
@@ -40437,7 +41193,7 @@ var init_godot_variant = __esm(() => {
|
|
|
40437
41193
|
});
|
|
40438
41194
|
|
|
40439
41195
|
// src/game-debugger.ts
|
|
40440
|
-
var PAUSE_MS = 30000, STEP_MS = 1e4, READ_MS = 5000, REPLIES;
|
|
41196
|
+
var PAUSE_MS = 30000, STEP_MS = 1e4, RUNTIME_ADDON2 = "res://addons/gdharness_runtime/", READ_MS = 5000, REPLIES;
|
|
40441
41197
|
var init_game_debugger = __esm(() => {
|
|
40442
41198
|
init_godot_variant();
|
|
40443
41199
|
REPLIES = new Set([
|
|
@@ -44043,168 +44799,6 @@ var init_godot_bridge = __esm(() => {
|
|
|
44043
44799
|
};
|
|
44044
44800
|
});
|
|
44045
44801
|
|
|
44046
|
-
// src/import-freshness.ts
|
|
44047
|
-
import { createHash as createHash6 } from "node:crypto";
|
|
44048
|
-
import { existsSync as existsSync28, readdirSync as readdirSync15, readFileSync as readFileSync28, statSync as statSync15 } from "node:fs";
|
|
44049
|
-
import { join as join33 } from "node:path";
|
|
44050
|
-
function md5Of(file) {
|
|
44051
|
-
let stats;
|
|
44052
|
-
try {
|
|
44053
|
-
stats = statSync15(file);
|
|
44054
|
-
} catch {
|
|
44055
|
-
return null;
|
|
44056
|
-
}
|
|
44057
|
-
const known = hashes.get(file);
|
|
44058
|
-
if (known !== undefined && known.size === stats.size && known.mtimeMs === stats.mtimeMs) {
|
|
44059
|
-
return known.md5;
|
|
44060
|
-
}
|
|
44061
|
-
let md5;
|
|
44062
|
-
try {
|
|
44063
|
-
md5 = createHash6("md5").update(readFileSync28(file)).digest("hex");
|
|
44064
|
-
} catch {
|
|
44065
|
-
return null;
|
|
44066
|
-
}
|
|
44067
|
-
hashes.set(file, { size: stats.size, mtimeMs: stats.mtimeMs, md5 });
|
|
44068
|
-
return md5;
|
|
44069
|
-
}
|
|
44070
|
-
function sectionOf(text, section) {
|
|
44071
|
-
const keys = new Map;
|
|
44072
|
-
let inside = false;
|
|
44073
|
-
for (const line of text.split(/\r?\n/)) {
|
|
44074
|
-
const header = /^\[([^\]]+)\]\s*$/.exec(line);
|
|
44075
|
-
if (header !== null) {
|
|
44076
|
-
inside = header[1] === section;
|
|
44077
|
-
continue;
|
|
44078
|
-
}
|
|
44079
|
-
const pair = inside ? /^([A-Za-z0-9_./]+)=(.*)$/.exec(line) : null;
|
|
44080
|
-
if (pair?.[1] !== undefined && pair[2] !== undefined) {
|
|
44081
|
-
keys.set(pair[1], pair[2]);
|
|
44082
|
-
}
|
|
44083
|
-
}
|
|
44084
|
-
return keys;
|
|
44085
|
-
}
|
|
44086
|
-
function unquoted(value) {
|
|
44087
|
-
return value === undefined ? "" : value.replace(/^"(.*)"$/, "$1");
|
|
44088
|
-
}
|
|
44089
|
-
function listed2(value) {
|
|
44090
|
-
return [...(value ?? "").matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((match) => match[1] ?? "");
|
|
44091
|
-
}
|
|
44092
|
-
function onDisk(projectPath, resPath) {
|
|
44093
|
-
return join33(projectPath, resPath.slice("res://".length));
|
|
44094
|
-
}
|
|
44095
|
-
function behind(projectPath, resPath, sidecar) {
|
|
44096
|
-
const remap = sectionOf(sidecar, "remap");
|
|
44097
|
-
const importer = unquoted(remap.get("importer"));
|
|
44098
|
-
if (importer === "keep" || importer === "skip") {
|
|
44099
|
-
return null;
|
|
44100
|
-
}
|
|
44101
|
-
const outputs = listed2(sectionOf(sidecar, "deps").get("dest_files"));
|
|
44102
|
-
if (outputs.some((output) => !existsSync28(onDisk(projectPath, output)))) {
|
|
44103
|
-
return "its imported output is not on disk";
|
|
44104
|
-
}
|
|
44105
|
-
const name = resPath.slice(resPath.lastIndexOf("/") + 1);
|
|
44106
|
-
const record = join33(projectPath, ".godot", "imported", `${name}-${createHash6("md5").update(resPath, "utf8").digest("hex")}.md5`);
|
|
44107
|
-
let recorded = "";
|
|
44108
|
-
try {
|
|
44109
|
-
recorded = /^source_md5="([0-9a-f]*)"/m.exec(readFileSync28(record, "utf8"))?.[1] ?? "";
|
|
44110
|
-
} catch {
|
|
44111
|
-
recorded = "";
|
|
44112
|
-
}
|
|
44113
|
-
if (recorded === "") {
|
|
44114
|
-
return "its import recorded no hash of its source";
|
|
44115
|
-
}
|
|
44116
|
-
const current = md5Of(onDisk(projectPath, resPath));
|
|
44117
|
-
return current === null || current === recorded ? null : "the source changed after it was imported";
|
|
44118
|
-
}
|
|
44119
|
-
function staleImports(projectPath) {
|
|
44120
|
-
const stale = [];
|
|
44121
|
-
const visit = (directory, resDirectory) => {
|
|
44122
|
-
if (existsSync28(join33(directory, ".gdignore")) || existsSync28(join33(directory, VERSION_MARKER))) {
|
|
44123
|
-
return;
|
|
44124
|
-
}
|
|
44125
|
-
let entries;
|
|
44126
|
-
try {
|
|
44127
|
-
entries = readdirSync15(directory, { withFileTypes: true });
|
|
44128
|
-
} catch {
|
|
44129
|
-
return;
|
|
44130
|
-
}
|
|
44131
|
-
const names = new Set(entries.map((entry) => entry.name));
|
|
44132
|
-
for (const entry of entries) {
|
|
44133
|
-
if (entry.name.startsWith(".")) {
|
|
44134
|
-
continue;
|
|
44135
|
-
}
|
|
44136
|
-
const resPath = `${resDirectory}${entry.name}`;
|
|
44137
|
-
if (entry.isDirectory()) {
|
|
44138
|
-
visit(join33(directory, entry.name), `${resPath}/`);
|
|
44139
|
-
continue;
|
|
44140
|
-
}
|
|
44141
|
-
if (!entry.isFile()) {
|
|
44142
|
-
continue;
|
|
44143
|
-
}
|
|
44144
|
-
if (entry.name.endsWith(".import")) {
|
|
44145
|
-
const source = resPath.slice(0, -".import".length);
|
|
44146
|
-
if (!names.has(entry.name.slice(0, -".import".length))) {
|
|
44147
|
-
continue;
|
|
44148
|
-
}
|
|
44149
|
-
let sidecar;
|
|
44150
|
-
try {
|
|
44151
|
-
sidecar = readFileSync28(join33(directory, entry.name), "utf8");
|
|
44152
|
-
} catch {
|
|
44153
|
-
continue;
|
|
44154
|
-
}
|
|
44155
|
-
const reason = behind(projectPath, source, sidecar);
|
|
44156
|
-
if (reason !== null) {
|
|
44157
|
-
stale.push({ path: source, reason });
|
|
44158
|
-
}
|
|
44159
|
-
continue;
|
|
44160
|
-
}
|
|
44161
|
-
const extension = entry.name.slice(entry.name.lastIndexOf(".") + 1).toLowerCase();
|
|
44162
|
-
if (IMPORTABLE.has(extension) && !names.has(`${entry.name}.import`)) {
|
|
44163
|
-
stale.push({ path: resPath, reason: "it has not been imported" });
|
|
44164
|
-
}
|
|
44165
|
-
}
|
|
44166
|
-
};
|
|
44167
|
-
visit(projectPath, "res://");
|
|
44168
|
-
return stale.sort((one, other) => one.path.localeCompare(other.path));
|
|
44169
|
-
}
|
|
44170
|
-
var IMPORTABLE_EXTENSIONS, IMPORTABLE, hashes;
|
|
44171
|
-
var init_import_freshness = __esm(() => {
|
|
44172
|
-
init_project_scan();
|
|
44173
|
-
IMPORTABLE_EXTENSIONS = [
|
|
44174
|
-
"png",
|
|
44175
|
-
"jpg",
|
|
44176
|
-
"jpeg",
|
|
44177
|
-
"webp",
|
|
44178
|
-
"svg",
|
|
44179
|
-
"bmp",
|
|
44180
|
-
"tga",
|
|
44181
|
-
"exr",
|
|
44182
|
-
"hdr",
|
|
44183
|
-
"dds",
|
|
44184
|
-
"ktx",
|
|
44185
|
-
"wav",
|
|
44186
|
-
"mp3",
|
|
44187
|
-
"ogg",
|
|
44188
|
-
"ttf",
|
|
44189
|
-
"otf",
|
|
44190
|
-
"ttc",
|
|
44191
|
-
"otc",
|
|
44192
|
-
"woff",
|
|
44193
|
-
"woff2",
|
|
44194
|
-
"pfb",
|
|
44195
|
-
"pfm",
|
|
44196
|
-
"fnt",
|
|
44197
|
-
"font",
|
|
44198
|
-
"glb",
|
|
44199
|
-
"gltf",
|
|
44200
|
-
"fbx",
|
|
44201
|
-
"obj",
|
|
44202
|
-
"dae"
|
|
44203
|
-
];
|
|
44204
|
-
IMPORTABLE = new Set(IMPORTABLE_EXTENSIONS);
|
|
44205
|
-
hashes = new Map;
|
|
44206
|
-
});
|
|
44207
|
-
|
|
44208
44802
|
// src/keeper-channel.ts
|
|
44209
44803
|
import { connect as connect2, createServer as createServer2 } from "node:net";
|
|
44210
44804
|
async function askTheKeeperToStop(address, withinMs) {
|
|
@@ -44636,7 +45230,7 @@ var init_prompts = __esm(() => {
|
|
|
44636
45230
|
});
|
|
44637
45231
|
|
|
44638
45232
|
// src/restart-note.ts
|
|
44639
|
-
import { mkdirSync as mkdirSync18, readFileSync as
|
|
45233
|
+
import { mkdirSync as mkdirSync18, readFileSync as readFileSync30, rmSync as rmSync15, writeFileSync as writeFileSync18 } from "node:fs";
|
|
44640
45234
|
import { dirname as dirname23, join as join35 } from "node:path";
|
|
44641
45235
|
function restartNotePath(projectPath) {
|
|
44642
45236
|
return join35(projectPath, ".godot", "gdharness-restart.json");
|
|
@@ -44653,7 +45247,7 @@ function noteRestartBegun(note) {
|
|
|
44653
45247
|
}
|
|
44654
45248
|
function restartOwed(projectPath) {
|
|
44655
45249
|
try {
|
|
44656
|
-
const parsed = JSON.parse(
|
|
45250
|
+
const parsed = JSON.parse(readFileSync30(restartNotePath(projectPath), "utf8"));
|
|
44657
45251
|
if (typeof parsed !== "object" || parsed === null) {
|
|
44658
45252
|
return null;
|
|
44659
45253
|
}
|
|
@@ -44679,7 +45273,7 @@ var init_restart_note = __esm(() => {
|
|
|
44679
45273
|
|
|
44680
45274
|
// src/run-record.ts
|
|
44681
45275
|
import { createHash as createHash7 } from "node:crypto";
|
|
44682
|
-
import { mkdirSync as mkdirSync19, openSync as openSync5, readdirSync as readdirSync16, readFileSync as
|
|
45276
|
+
import { mkdirSync as mkdirSync19, openSync as openSync5, readdirSync as readdirSync16, readFileSync as readFileSync31, rmSync as rmSync16, statSync as statSync17 } from "node:fs";
|
|
44683
45277
|
import { basename as basename10, join as join36, resolve as resolve8 } from "node:path";
|
|
44684
45278
|
function runsDirectory() {
|
|
44685
45279
|
return join36(runtimeDirectory(), "runs");
|
|
@@ -44703,7 +45297,7 @@ function writeLatestRun(servedBy, latest) {
|
|
|
44703
45297
|
}
|
|
44704
45298
|
function readLatestRun(servedBy, directory = runtimeDirectory()) {
|
|
44705
45299
|
try {
|
|
44706
|
-
const fields = JSON.parse(
|
|
45300
|
+
const fields = JSON.parse(readFileSync31(latestRunPath(servedBy, directory), "utf8"));
|
|
44707
45301
|
if (typeof fields !== "object" || fields === null) {
|
|
44708
45302
|
return null;
|
|
44709
45303
|
}
|
|
@@ -44752,7 +45346,7 @@ function readEditorRunNote(projectPath) {
|
|
|
44752
45346
|
function editorNoteAt(path) {
|
|
44753
45347
|
let parsed;
|
|
44754
45348
|
try {
|
|
44755
|
-
parsed = JSON.parse(
|
|
45349
|
+
parsed = JSON.parse(readFileSync31(path, "utf8"));
|
|
44756
45350
|
} catch {
|
|
44757
45351
|
return null;
|
|
44758
45352
|
}
|
|
@@ -44802,7 +45396,7 @@ function everyRunRecord() {
|
|
|
44802
45396
|
function recordAt(path) {
|
|
44803
45397
|
let parsed;
|
|
44804
45398
|
try {
|
|
44805
|
-
parsed = JSON.parse(
|
|
45399
|
+
parsed = JSON.parse(readFileSync31(path, "utf8"));
|
|
44806
45400
|
} catch {
|
|
44807
45401
|
return null;
|
|
44808
45402
|
}
|
|
@@ -44861,7 +45455,7 @@ async function runningAs(pid) {
|
|
|
44861
45455
|
const began = await startedAt(pid, null);
|
|
44862
45456
|
const when = began === null ? {} : { startedAt: began };
|
|
44863
45457
|
if (process.platform === "linux") {
|
|
44864
|
-
const raw =
|
|
45458
|
+
const raw = readFileSync31(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
44865
45459
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
44866
45460
|
}
|
|
44867
45461
|
const args = (await runHidden("ps", ["-p", String(pid), "-o", "args="], { timeout: 15000 })).stdout.trim();
|
|
@@ -45049,10 +45643,10 @@ var init_run_record = __esm(() => {
|
|
|
45049
45643
|
});
|
|
45050
45644
|
|
|
45051
45645
|
// src/script-profile.ts
|
|
45052
|
-
import { readFileSync as
|
|
45646
|
+
import { readFileSync as readFileSync32 } from "node:fs";
|
|
45053
45647
|
function readProfileFile(path) {
|
|
45054
45648
|
try {
|
|
45055
|
-
const parsed = JSON.parse(
|
|
45649
|
+
const parsed = JSON.parse(readFileSync32(path, "utf8"));
|
|
45056
45650
|
return typeof parsed === "object" && typeof parsed.connected === "boolean" ? parsed : null;
|
|
45057
45651
|
} catch {
|
|
45058
45652
|
return null;
|
|
@@ -45109,7 +45703,7 @@ function writeProfileMark(path, mark) {
|
|
|
45109
45703
|
}
|
|
45110
45704
|
function readProfileMark(path) {
|
|
45111
45705
|
try {
|
|
45112
|
-
const parsed = JSON.parse(
|
|
45706
|
+
const parsed = JSON.parse(readFileSync32(path, "utf8"));
|
|
45113
45707
|
return typeof parsed === "object" && typeof parsed.totals === "object" ? parsed : null;
|
|
45114
45708
|
} catch {
|
|
45115
45709
|
return null;
|
|
@@ -45251,8 +45845,9 @@ function callersOf(totals, name, scripts) {
|
|
|
45251
45845
|
property
|
|
45252
45846
|
};
|
|
45253
45847
|
}
|
|
45254
|
-
var QUEUE_SETTING = "limits/debugger/max_queued_messages", QUEUE_DEFAULT = 2048, QUEUE_HEADROOM = 512, UNNAMED_PREFIX = "#", PROFILE_FROM_START = "GDHARNESS_PROFILE_FROM_START", ms = (seconds) => Math.round(seconds * 1e6) / 1000, RUNTIME_SCRIPT = "gdharness_runtime/runtime_autoload.gd", PROFILER_PROBE = "_profiler_probe", PROFILER_CALIBRATOR = "_calibrate_the_profiler",
|
|
45848
|
+
var QUEUE_SETTING = "limits/debugger/max_queued_messages", QUEUE_DEFAULT = 2048, QUEUE_HEADROOM = 512, UNNAMED_PREFIX = "#", PROFILE_FROM_START = "GDHARNESS_PROFILE_FROM_START", ms = (seconds) => Math.round(seconds * 1e6) / 1000, RUNTIME_SCRIPT = "gdharness_runtime/runtime_autoload.gd", PROFILER_PROBE = "_profiler_probe", PROFILER_CALIBRATOR = "_calibrate_the_profiler", CALL_SITES = 40;
|
|
45255
45849
|
var init_script_profile = __esm(() => {
|
|
45850
|
+
init_game_debugger();
|
|
45256
45851
|
init_gdscript_source();
|
|
45257
45852
|
init_godot_variant();
|
|
45258
45853
|
init_resources();
|
|
@@ -45276,10 +45871,10 @@ var init_server_instructions = __esm(() => {
|
|
|
45276
45871
|
});
|
|
45277
45872
|
|
|
45278
45873
|
// src/test-selection.ts
|
|
45279
|
-
import { existsSync as existsSync29, readFileSync as
|
|
45874
|
+
import { existsSync as existsSync29, readFileSync as readFileSync33 } from "node:fs";
|
|
45280
45875
|
import { basename as basename11, join as join37 } from "node:path";
|
|
45281
45876
|
function casesIn(projectPath, suite) {
|
|
45282
|
-
const text =
|
|
45877
|
+
const text = readFileSync33(join37(projectPath, suite.slice("res://".length)), "utf8");
|
|
45283
45878
|
return [...text.matchAll(/^func\s+(test_\w*)\s*\(/gm)].map((match) => match[1] ?? "");
|
|
45284
45879
|
}
|
|
45285
45880
|
function suitesUnder(projectPath, asked) {
|
|
@@ -45351,7 +45946,7 @@ import {
|
|
|
45351
45946
|
mkdirSync as mkdirSync20,
|
|
45352
45947
|
openSync as openSync6,
|
|
45353
45948
|
readdirSync as readdirSync17,
|
|
45354
|
-
readFileSync as
|
|
45949
|
+
readFileSync as readFileSync34,
|
|
45355
45950
|
readSync as readSync3,
|
|
45356
45951
|
rmdirSync as rmdirSync2,
|
|
45357
45952
|
statSync as statSync18,
|
|
@@ -45370,6 +45965,25 @@ function waitFromEnv(variable, fallback) {
|
|
|
45370
45965
|
function greetingWaitMs() {
|
|
45371
45966
|
return waitFromEnv("GDHARNESS_GREETING_WAIT_MS", 1e4);
|
|
45372
45967
|
}
|
|
45968
|
+
function pictureToSend(pngPath, jpegPath) {
|
|
45969
|
+
const png = readFileSync34(pngPath);
|
|
45970
|
+
const beside = pngPath.replace(/\.png$/, ".jpg");
|
|
45971
|
+
const jpeg = jpegPath !== undefined && normalize2(jpegPath) === normalize2(beside) && existsSync30(beside) ? readFileSync34(beside) : null;
|
|
45972
|
+
const sent = jpeg !== null && jpeg.length < png.length ? jpeg : png;
|
|
45973
|
+
return {
|
|
45974
|
+
data: sent.toString("base64"),
|
|
45975
|
+
mimeType: sent === png ? "image/png" : "image/jpeg",
|
|
45976
|
+
bytes: sent.length,
|
|
45977
|
+
pngBytes: png.length
|
|
45978
|
+
};
|
|
45979
|
+
}
|
|
45980
|
+
function kilobytes(bytes) {
|
|
45981
|
+
return `${String(Math.max(1, Math.round(bytes / 1024)))} KB`;
|
|
45982
|
+
}
|
|
45983
|
+
function savedScenesOf(asked, as = "savedScenes") {
|
|
45984
|
+
const saved = readStringArray(asParams(JSON.parse(asked.content[0]?.text ?? "{}")), "saved");
|
|
45985
|
+
return saved === undefined ? {} : { [as]: saved };
|
|
45986
|
+
}
|
|
45373
45987
|
function restartUnderway(note, now = Date.now()) {
|
|
45374
45988
|
return alive(note.byPid) && now - Date.parse(note.quitAt) < editorRestartTimeoutMs() + 30000;
|
|
45375
45989
|
}
|
|
@@ -45787,7 +46401,7 @@ function endedToStartThis(ended) {
|
|
|
45787
46401
|
return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
|
|
45788
46402
|
}
|
|
45789
46403
|
function lineBeyondScript(absolutePath, relativePath, line) {
|
|
45790
|
-
const text =
|
|
46404
|
+
const text = readFileSync34(absolutePath, "utf8");
|
|
45791
46405
|
const lines = text === "" ? 0 : text.split(`
|
|
45792
46406
|
`).length - (text.endsWith(`
|
|
45793
46407
|
`) ? 1 : 0);
|
|
@@ -45987,7 +46601,7 @@ function notRunNote(notRun, failFast) {
|
|
|
45987
46601
|
function exportPresetRefusal(projectPath, preset) {
|
|
45988
46602
|
let text;
|
|
45989
46603
|
try {
|
|
45990
|
-
text =
|
|
46604
|
+
text = readFileSync34(join38(projectPath, "export_presets.cfg"), "utf8");
|
|
45991
46605
|
} catch {
|
|
45992
46606
|
return `This project has no export_presets.cfg, so it has no preset '${preset}' to export with. project_export add_preset writes one, as the editor's Project > Export does.`;
|
|
45993
46607
|
}
|
|
@@ -46350,7 +46964,7 @@ function profileFrameFunctions(projectPath) {
|
|
|
46350
46964
|
let projectGodot = null;
|
|
46351
46965
|
if (projectPath !== null) {
|
|
46352
46966
|
try {
|
|
46353
|
-
projectGodot =
|
|
46967
|
+
projectGodot = readFileSync34(join38(projectPath, "project.godot"), "utf8");
|
|
46354
46968
|
} catch {}
|
|
46355
46969
|
}
|
|
46356
46970
|
return frameFunctionsFor(queuedMessagesOf(projectGodot));
|
|
@@ -46374,7 +46988,7 @@ function projectSourceOf(projectPath) {
|
|
|
46374
46988
|
let source = null;
|
|
46375
46989
|
if (inside.ok) {
|
|
46376
46990
|
try {
|
|
46377
|
-
source =
|
|
46991
|
+
source = readFileSync34(inside.absolutePath, "utf8");
|
|
46378
46992
|
} catch {}
|
|
46379
46993
|
}
|
|
46380
46994
|
read.set(script, source);
|
|
@@ -46529,7 +47143,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
|
|
|
46529
47143
|
}
|
|
46530
47144
|
function gdUnitFailsOnScriptErrors(projectPath) {
|
|
46531
47145
|
try {
|
|
46532
|
-
return settingKeys(
|
|
47146
|
+
return settingKeys(readFileSync34(join38(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
|
|
46533
47147
|
} catch {
|
|
46534
47148
|
return true;
|
|
46535
47149
|
}
|
|
@@ -46558,12 +47172,13 @@ function elsewhereIn(projectPath, asked) {
|
|
|
46558
47172
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
46559
47173
|
}
|
|
46560
47174
|
function testPathsIn(projectPath, given) {
|
|
46561
|
-
const
|
|
47175
|
+
const defaults = TEST_DIRECTORIES.filter((directory) => existsSync30(join38(projectPath, directory)));
|
|
47176
|
+
const entries = given === undefined || given === null || given === "" ? defaults.length > 0 ? defaults : TEST_DIRECTORIES : given;
|
|
46562
47177
|
const listed = Array.isArray(entries) ? entries : [entries];
|
|
46563
47178
|
if (listed.length === 0) {
|
|
46564
47179
|
return {
|
|
46565
47180
|
ok: false,
|
|
46566
|
-
reason: "path is an empty list: name a suite file or a directory, or leave path out for test."
|
|
47181
|
+
reason: "path is an empty list: name a suite file or a directory, or leave path out for tests and test."
|
|
46567
47182
|
};
|
|
46568
47183
|
}
|
|
46569
47184
|
const resolved = [];
|
|
@@ -46665,11 +47280,11 @@ function scriptsUnder(directory, prefix) {
|
|
|
46665
47280
|
return found;
|
|
46666
47281
|
}
|
|
46667
47282
|
function mainSceneOf(projectFile) {
|
|
46668
|
-
const scene = parseProjectGodot(
|
|
47283
|
+
const scene = parseProjectGodot(readFileSync34(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
46669
47284
|
return typeof scene === "string" && scene !== "" ? scene : null;
|
|
46670
47285
|
}
|
|
46671
47286
|
function editorPlaysHeadless(projectFile) {
|
|
46672
|
-
const runArgs = parseProjectGodot(
|
|
47287
|
+
const runArgs = parseProjectGodot(readFileSync34(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
46673
47288
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
46674
47289
|
}
|
|
46675
47290
|
function ownArguments(params) {
|
|
@@ -48496,7 +49111,7 @@ class GodotServer {
|
|
|
48496
49111
|
if (!engine.ok) {
|
|
48497
49112
|
return engine.response;
|
|
48498
49113
|
}
|
|
48499
|
-
const application = parseProjectGodot(
|
|
49114
|
+
const application = parseProjectGodot(readFileSync34(project.value.file, "utf8"))["application"] ?? {};
|
|
48500
49115
|
const name = application["config/name"];
|
|
48501
49116
|
const mainScene = application["run/main_scene"];
|
|
48502
49117
|
const info = {
|
|
@@ -48767,6 +49382,8 @@ class GodotServer {
|
|
|
48767
49382
|
const userData = scratchDirectory("tests");
|
|
48768
49383
|
const suitesFile = join38(userData, "suites.txt");
|
|
48769
49384
|
const skippingFile = join38(userData, "skipping.txt");
|
|
49385
|
+
const engineLog = join38(userData, "engine.log");
|
|
49386
|
+
const expectedFile = join38(userData, "expected.json");
|
|
48770
49387
|
writeFileSync19(suitesFile, `${(selection.value?.suites ?? asked).join(`
|
|
48771
49388
|
`)}
|
|
48772
49389
|
`);
|
|
@@ -48778,6 +49395,8 @@ class GodotServer {
|
|
|
48778
49395
|
"--path",
|
|
48779
49396
|
project.value.path,
|
|
48780
49397
|
...flags.value,
|
|
49398
|
+
"--log-file",
|
|
49399
|
+
engineLog,
|
|
48781
49400
|
"-s",
|
|
48782
49401
|
this.listedRunner,
|
|
48783
49402
|
"--ignoreHeadlessMode",
|
|
@@ -48791,7 +49410,9 @@ class GodotServer {
|
|
|
48791
49410
|
suitesFile,
|
|
48792
49411
|
"--skipping-in",
|
|
48793
49412
|
skippingFile,
|
|
48794
|
-
"--html-report-off"
|
|
49413
|
+
"--html-report-off",
|
|
49414
|
+
"--expected-errors-out",
|
|
49415
|
+
expectedFile
|
|
48795
49416
|
];
|
|
48796
49417
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
48797
49418
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
@@ -48846,11 +49467,13 @@ class GodotServer {
|
|
|
48846
49467
|
const reportsDir = join38(project.value.path, ".godot", ...ours.split("/"));
|
|
48847
49468
|
let report = null;
|
|
48848
49469
|
let reportProblem = null;
|
|
49470
|
+
const casePrinted = readRun(existsSync30(engineLog) ? readFileSync34(engineLog, "utf8") : "");
|
|
49471
|
+
const declaredErrors = readExpected(expectedFile);
|
|
48849
49472
|
try {
|
|
48850
49473
|
const written = existsSync30(reportsDir) && notCompiled === null ? readdirSync17(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
48851
49474
|
const newest = written.at(-1);
|
|
48852
49475
|
if (newest !== undefined) {
|
|
48853
|
-
report = parseJUnit(
|
|
49476
|
+
report = parseJUnit(readFileSync34(join38(reportsDir, newest, "results.xml"), "utf8"));
|
|
48854
49477
|
}
|
|
48855
49478
|
} catch (error) {
|
|
48856
49479
|
reportProblem = errorMessage(error);
|
|
@@ -48961,7 +49584,44 @@ class GodotServer {
|
|
|
48961
49584
|
orphans: suite.orphans,
|
|
48962
49585
|
message: `${suite.orphans} node${suite.orphans === 1 ? "" : "s"} were still in the tree when this suite finished. A node built in a test and neither freed nor added to the tree is the usual cause; free it, or add it with auto_free().`
|
|
48963
49586
|
}));
|
|
48964
|
-
const
|
|
49587
|
+
const judgedErrors = unexpectedErrors(casePrinted, declaredErrors, [...casePrinted.suites.keys()]);
|
|
49588
|
+
const uncaughtTexts = new Set(uncaught.map((entry) => entry.text));
|
|
49589
|
+
const undeclaredPrinted = judgedErrors.printed.filter((error) => !uncaughtTexts.has(error.text));
|
|
49590
|
+
const undeclaredFolded = new Map;
|
|
49591
|
+
for (const error of undeclaredPrinted) {
|
|
49592
|
+
const key = `${error.suite ?? ""}
|
|
49593
|
+
${error.during ?? ""}
|
|
49594
|
+
${error.text}`;
|
|
49595
|
+
const seen = undeclaredFolded.get(key);
|
|
49596
|
+
if (seen === undefined) {
|
|
49597
|
+
undeclaredFolded.set(key, {
|
|
49598
|
+
suite: error.suite ?? undefined,
|
|
49599
|
+
case: error.during ?? undefined,
|
|
49600
|
+
text: error.text,
|
|
49601
|
+
times: 1
|
|
49602
|
+
});
|
|
49603
|
+
} else {
|
|
49604
|
+
seen.times += 1;
|
|
49605
|
+
}
|
|
49606
|
+
}
|
|
49607
|
+
const undeclaredAll = [...undeclaredFolded.values()].map((error) => ({
|
|
49608
|
+
...error,
|
|
49609
|
+
times: error.times > 1 ? error.times : undefined
|
|
49610
|
+
}));
|
|
49611
|
+
const undeclaredErrors = undeclaredAll.slice(0, MOST_UNDECLARED_ERRORS);
|
|
49612
|
+
const declaredNotPrinted = judgedErrors.neverPrinted.map((missing) => ({
|
|
49613
|
+
suite: missing.suite,
|
|
49614
|
+
case: missing.during,
|
|
49615
|
+
text: missing.text
|
|
49616
|
+
}));
|
|
49617
|
+
const erring = (path) => ({
|
|
49618
|
+
undeclared: undeclaredPrinted.filter((error) => error.suite === path).length,
|
|
49619
|
+
notPrinted: judgedErrors.neverPrinted.filter((missing) => missing.suite === path).length
|
|
49620
|
+
});
|
|
49621
|
+
const declaredAndPrinted = casePrinted.errors.length - judgedErrors.printed.length;
|
|
49622
|
+
const ranIn = (suite) => suite.path === null ? suite.tests : casePrinted.suites.get(suite.path)?.cases.length ?? suite.tests;
|
|
49623
|
+
const notRunIn = (suite) => Math.max(0, suite.discovered - ranIn(suite));
|
|
49624
|
+
const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.hookFailures > 0 || suite.hookErrors > 0 || suite.skipped > 0 || orphansIn(suite.path) > 0 || erring(suite.path).undeclared + erring(suite.path).notPrinted > 0);
|
|
48965
49625
|
const hooksPrinted = hookFailuresPrinted(said);
|
|
48966
49626
|
const hookFailures = report.suites.filter((suite) => suite.hookFailures + suite.hookErrors > 0).flatMap((suite) => {
|
|
48967
49627
|
const printedHere = hooksPrinted.filter((hook) => hook.path === suite.path);
|
|
@@ -48973,16 +49633,24 @@ class GodotServer {
|
|
|
48973
49633
|
}
|
|
48974
49634
|
];
|
|
48975
49635
|
});
|
|
48976
|
-
const notRun = report.suites.reduce((sum, suite) => sum +
|
|
49636
|
+
const notRun = report.suites.reduce((sum, suite) => sum + notRunIn(suite), 0);
|
|
48977
49637
|
const uncaughtFolded = foldedRepeats(uncaught.map(aboveTheRunner));
|
|
48978
49638
|
const listedAsUncaught = new Set(uncaughtFolded.map((entry) => entry.index));
|
|
48979
49639
|
const notApplied = ignoredYetRan(report, ignored);
|
|
49640
|
+
const scripts = ignored.length === 0 ? null : new ProjectScripts(project.value.path);
|
|
49641
|
+
const ignoreRead = ignoredLeftOut(report, ignored, asked.flatMap((path) => scripts === null ? [] : path.endsWith(".gd") ? scripts.suite(path) ?? [] : scripts.suitesUnder([path])));
|
|
49642
|
+
const unmatched = ignoreRead.unmatched.filter((entry) => !notApplied.some((one) => one.entry === entry));
|
|
48980
49643
|
const testsNotRun = [
|
|
48981
49644
|
...new Set((selection.value?.selected ?? []).filter((one) => !report.suites.some((suite) => (suite.path === one.suite || suite.name === basename12(one.suite, ".gd")) && suite.cases.some((ran) => ran.name === one.test || new RegExp(`^${one.test}\\W`).test(ran.name)))).map((one) => `${basename12(one.suite, ".gd")}:${one.test}`))
|
|
48982
49645
|
];
|
|
48983
49646
|
return this.jsonTextResponse({
|
|
48984
|
-
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && missing.length === 0 && testsNotRun.length === 0,
|
|
48985
|
-
verdict:
|
|
49647
|
+
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && undeclaredPrinted.length === 0 && declaredNotPrinted.length === 0 && missing.length === 0 && testsNotRun.length === 0,
|
|
49648
|
+
verdict: [
|
|
49649
|
+
verdict === "passed" && undeclaredPrinted.length + declaredNotPrinted.length > 0 ? undefined : verdict,
|
|
49650
|
+
undeclaredPrinted.length > 0 ? `${undeclaredPrinted.length === 1 ? "one engine error" : `${String(undeclaredPrinted.length)} engine errors`} no suite declared` : undefined,
|
|
49651
|
+
declaredNotPrinted.length > 0 ? `${declaredNotPrinted.length === 1 ? "one declared error" : `${String(declaredNotPrinted.length)} declared errors`} never printed` : undefined,
|
|
49652
|
+
testsNotRun.length > 0 ? `not run: ${testsNotRun.join(", ")}` : undefined
|
|
49653
|
+
].filter((part) => part !== undefined).join("; "),
|
|
48986
49654
|
...missing.length > 0 ? { missing } : {},
|
|
48987
49655
|
...testsNotRun.length > 0 ? { testsNotRun } : {},
|
|
48988
49656
|
...timedOut ? { timedOut, hung, silentForMs } : {},
|
|
@@ -48998,10 +49666,15 @@ class GodotServer {
|
|
|
48998
49666
|
failedWithoutDetail: briefed > 0 ? briefed : undefined,
|
|
48999
49667
|
hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
|
|
49000
49668
|
uncaughtScriptErrors: uncaughtFolded.length > 0 ? uncaughtFolded : undefined,
|
|
49669
|
+
undeclaredErrors: undeclaredErrors.length > 0 ? undeclaredErrors : undefined,
|
|
49670
|
+
undeclaredErrorsOmitted: undeclaredAll.length > undeclaredErrors.length ? undeclaredAll.length - undeclaredErrors.length : undefined,
|
|
49671
|
+
declaredNotPrinted: declaredNotPrinted.length > 0 ? declaredNotPrinted : undefined,
|
|
49001
49672
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
49002
49673
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
49003
49674
|
notRun: notRun > 0 ? notRun : undefined,
|
|
49004
49675
|
ignoreNotApplied: notApplied.length > 0 ? notApplied : undefined,
|
|
49676
|
+
ignored: ignoreRead.leftOut.length > 0 ? ignoreRead.leftOut : undefined,
|
|
49677
|
+
ignoreUnmatched: unmatched.length > 0 ? unmatched : undefined,
|
|
49005
49678
|
...printedLines,
|
|
49006
49679
|
savesNote,
|
|
49007
49680
|
...scanned,
|
|
@@ -49010,14 +49683,19 @@ class GodotServer {
|
|
|
49010
49683
|
testsNotRun.length > 0 ? `${testsNotRun.join(", ")} ${testsNotRun.length === 1 ? "was" : "were"} asked for and not run, so the run is not called passed: gdUnit4 runs a test_ function only in a script that extends GdUnitTestSuite, and leaves out a suite whose script does not load.` : undefined,
|
|
49011
49684
|
briefed > 0 ? `${briefed} of the ${failed.length} failing cases, those with the largest reports, are named with their first message only, since their reports would have made the answer too large to deliver; pass one case's suite path to see its reports.` : undefined,
|
|
49012
49685
|
notApplied.length > 0 ? "An ignore entry named a case that ran all the same: gdUnit4 leaves a case out only as suite_name:test_name, and reads an entry with no colon as a suite name or a piece of a suite path. ignoreNotApplied gives the form for each case that ran." : undefined,
|
|
49686
|
+
unmatched.length > 0 ? `${unmatched.join(", ")} ${unmatched.length === 1 ? "names" : "name"} no suite or case under the paths run, so ${unmatched.length === 1 ? "it" : "they"} left nothing out.` : undefined,
|
|
49013
49687
|
uncaughtNote(uncaught),
|
|
49688
|
+
undeclaredErrors.length > 0 ? `An engine error a suite does not declare fails the run, as gdharness test judges it; ${declaringAdvice(undeclaredErrors[0]?.case ?? "test_name")}, once for each time it prints it.` : undefined,
|
|
49689
|
+
declaredNotPrinted.length > 0 ? "A declared error the case did not print fails the run too, since the case no longer reaches what it was written to test." : undefined,
|
|
49014
49690
|
ranAs.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRunNote(notRun, readBoolean(args, "failFast") === true)
|
|
49015
49691
|
].filter((part) => part !== undefined).join(" ") || undefined,
|
|
49016
49692
|
suites: unclean.map((suite) => ({
|
|
49017
49693
|
name: suite.name,
|
|
49018
49694
|
path: suite.path,
|
|
49019
|
-
tests: suite
|
|
49020
|
-
notRun: suite
|
|
49695
|
+
tests: ranIn(suite),
|
|
49696
|
+
notRun: notRunIn(suite) > 0 ? notRunIn(suite) : undefined,
|
|
49697
|
+
undeclaredErrors: erring(suite.path).undeclared > 0 ? erring(suite.path).undeclared : undefined,
|
|
49698
|
+
declaredNotPrinted: erring(suite.path).notPrinted > 0 ? erring(suite.path).notPrinted : undefined,
|
|
49021
49699
|
failures: suite.failures,
|
|
49022
49700
|
errors: suite.errors,
|
|
49023
49701
|
hookFailures: suite.hookFailures + suite.hookErrors > 0 ? suite.hookFailures + suite.hookErrors : undefined,
|
|
@@ -49026,7 +49704,7 @@ class GodotServer {
|
|
|
49026
49704
|
time: suite.time
|
|
49027
49705
|
})),
|
|
49028
49706
|
suitesPassed: report.suites.length - unclean.length,
|
|
49029
|
-
engineErrors: run.log.count("error"),
|
|
49707
|
+
engineErrors: Math.max(0, run.log.count("error") - declaredAndPrinted),
|
|
49030
49708
|
engineWarnings: run.log.count("warning"),
|
|
49031
49709
|
engineEntries: engineEntriesShown.map((entry) => listedAsUncaught.has(entry.index) ? { ...entry, detail: undefined, detailUnder: "uncaughtScriptErrors" } : entry),
|
|
49032
49710
|
engineEntriesOmitted: engineEntries.length > engineEntriesShown.length ? engineEntries.length - engineEntriesShown.length : undefined,
|
|
@@ -49242,7 +49920,7 @@ class GodotServer {
|
|
|
49242
49920
|
return null;
|
|
49243
49921
|
}
|
|
49244
49922
|
try {
|
|
49245
|
-
return
|
|
49923
|
+
return readFileSync34(contained.absolutePath, "utf8");
|
|
49246
49924
|
} catch {
|
|
49247
49925
|
return null;
|
|
49248
49926
|
}
|
|
@@ -49336,7 +50014,7 @@ class GodotServer {
|
|
|
49336
50014
|
return null;
|
|
49337
50015
|
}
|
|
49338
50016
|
try {
|
|
49339
|
-
return
|
|
50017
|
+
return readFileSync34(contained.absolutePath, "utf8");
|
|
49340
50018
|
} catch {
|
|
49341
50019
|
return null;
|
|
49342
50020
|
}
|
|
@@ -49772,8 +50450,9 @@ class GodotServer {
|
|
|
49772
50450
|
if (servers.dapListening !== false && servers.lspListening !== false) {
|
|
49773
50451
|
return { servers, report: {} };
|
|
49774
50452
|
}
|
|
49775
|
-
const { rebound,
|
|
50453
|
+
const { rebound, before, since } = await this.editorServersSaid(servers.dapListening === false);
|
|
49776
50454
|
const after = rebound ? await this.editorServersListening() : servers;
|
|
50455
|
+
const said = [...before, ...since];
|
|
49777
50456
|
return {
|
|
49778
50457
|
servers: after,
|
|
49779
50458
|
report: {
|
|
@@ -49791,17 +50470,18 @@ class GodotServer {
|
|
|
49791
50470
|
}
|
|
49792
50471
|
return {
|
|
49793
50472
|
rebound: readBoolean(answer, "rebound") === true,
|
|
49794
|
-
|
|
50473
|
+
before: readStringArray(answer, "said") ?? [],
|
|
50474
|
+
since: readStringArray(answer, "saidSince") ?? []
|
|
49795
50475
|
};
|
|
49796
50476
|
}
|
|
49797
50477
|
async handleRebindEditorServers() {
|
|
49798
50478
|
if (!this.godotBridge.getStatus().connected) {
|
|
49799
|
-
|
|
49800
|
-
|
|
49801
|
-
|
|
49802
|
-
|
|
50479
|
+
const refused = await this.noEditorYet({}, []);
|
|
50480
|
+
if (refused !== null) {
|
|
50481
|
+
return refused;
|
|
50482
|
+
}
|
|
49803
50483
|
}
|
|
49804
|
-
const { rebound, said } = await this.editorServersSaid(true);
|
|
50484
|
+
const { rebound, since: said } = await this.editorServersSaid(true);
|
|
49805
50485
|
if (!rebound) {
|
|
49806
50486
|
return this.createErrorResponse("The editor has no debug adapter to bind again: no DebugAdapterServer is in its tree.", ["editor_status says what the editor serves"]);
|
|
49807
50487
|
}
|
|
@@ -49817,7 +50497,7 @@ class GodotServer {
|
|
|
49817
50497
|
return null;
|
|
49818
50498
|
}
|
|
49819
50499
|
try {
|
|
49820
|
-
return settingKeys(
|
|
50500
|
+
return settingKeys(readFileSync34(join38(projectPath, "project.godot"), "utf8"));
|
|
49821
50501
|
} catch {
|
|
49822
50502
|
return null;
|
|
49823
50503
|
}
|
|
@@ -49865,6 +50545,7 @@ class GodotServer {
|
|
|
49865
50545
|
serverVersion: SERVER_VERSION,
|
|
49866
50546
|
addonIsStale: this.godotBridge.anotherRelease() !== undefined,
|
|
49867
50547
|
staleNote: this.godotBridge.anotherRelease(),
|
|
50548
|
+
...savedScenesOf(asked),
|
|
49868
50549
|
...this.whatTheEditorDropped(settingsBefore, after),
|
|
49869
50550
|
...servers,
|
|
49870
50551
|
...rebinding,
|
|
@@ -49908,6 +50589,7 @@ class GodotServer {
|
|
|
49908
50589
|
closed: true,
|
|
49909
50590
|
editorPid: pid,
|
|
49910
50591
|
projectPath: before.projectPath,
|
|
50592
|
+
...savedScenesOf(asked),
|
|
49911
50593
|
...this.whatTheEditorDropped(settingsBefore, this.settingKeysOf(before.projectPath)),
|
|
49912
50594
|
tookMs: Date.now() - began
|
|
49913
50595
|
});
|
|
@@ -49991,7 +50673,7 @@ class GodotServer {
|
|
|
49991
50673
|
if (opened.error !== null) {
|
|
49992
50674
|
return this.createErrorResponse(`The editor was asked to go and could not be started again: ${opened.error}`, [`editor_launch opens one on ${projectPath}`]);
|
|
49993
50675
|
}
|
|
49994
|
-
return this.jsonTextResponse({ restarting: true, pid: opened.pid });
|
|
50676
|
+
return this.jsonTextResponse({ restarting: true, pid: opened.pid, ...savedScenesOf(asked, "saved") });
|
|
49995
50677
|
}
|
|
49996
50678
|
async waitForBridge(reached, deadline) {
|
|
49997
50679
|
for (;; ) {
|
|
@@ -50050,7 +50732,7 @@ class GodotServer {
|
|
|
50050
50732
|
dapPort: ports.dap,
|
|
50051
50733
|
arguments: opened.arguments,
|
|
50052
50734
|
finishesRestart: unfinished === null ? undefined : `This is the launch half of a restart begun at ${unfinished.quitAt} by server pid ${unfinished.byPid}, which quit editor pid ${unfinished.editorPid ?? "unknown"} and was ended before it could start one.`,
|
|
50053
|
-
settingsNote: before === null ? "project.godot could not be read before this editor was opened, so nothing here will be able to say what the import saved away. Opening a project rewrites the file and drops any key sitting at its own default." : before.size === 0 ? undefined : "Opening a project imports it and saves project.godot, and Godot drops any key sitting at its own default. editor_status names anything lost under settingsDropped once this editor has connected."
|
|
50735
|
+
settingsNote: before === null ? "project.godot could not be read before this editor was opened, so nothing here will be able to say what the import saved away. Opening a project rewrites the file and drops any key sitting at its own default." : before.size === 0 ? undefined : "Opening a project imports it and saves project.godot, and Godot drops any key sitting at its own default. editor_status names anything lost under settingsDropped once this editor has connected, and leaves settingsDropped out when nothing was."
|
|
50054
50736
|
});
|
|
50055
50737
|
}
|
|
50056
50738
|
async editorsNotYetConnected(projectPath) {
|
|
@@ -50542,7 +51224,7 @@ class GodotServer {
|
|
|
50542
51224
|
return 0;
|
|
50543
51225
|
}
|
|
50544
51226
|
try {
|
|
50545
|
-
return reportAlreadyInTranscript(
|
|
51227
|
+
return reportAlreadyInTranscript(readFileSync34(game.transcript), readFileSync34(report));
|
|
50546
51228
|
} catch (error) {
|
|
50547
51229
|
this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
|
|
50548
51230
|
return 0;
|
|
@@ -52627,7 +53309,7 @@ class GodotServer {
|
|
|
52627
53309
|
}
|
|
52628
53310
|
const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
52629
53311
|
const declared = new RegExp(`^\\s*(?:@\\w+(?:\\([^)]*\\))?\\s+)*class_name\\s+${escaped}\\b`, "m");
|
|
52630
|
-
if (declared.test(
|
|
53312
|
+
if (declared.test(readFileSync34(scriptPath, "utf8"))) {
|
|
52631
53313
|
return null;
|
|
52632
53314
|
}
|
|
52633
53315
|
const checked = await this.classesTheEditorCannotSee(args);
|
|
@@ -52692,6 +53374,10 @@ class GodotServer {
|
|
|
52692
53374
|
"editor_launch open starts another"
|
|
52693
53375
|
]);
|
|
52694
53376
|
}
|
|
53377
|
+
const closed = this.closedHere;
|
|
53378
|
+
if (closed !== null && (project === null || (await this.editorsNotYetConnected(project) ?? []).length === 0)) {
|
|
53379
|
+
return this.createErrorResponse(`Editor not connected: this server closed the editor, pid ${closed.editorPid}, on request at ${closed.at}, so nothing is coming until one is opened.`, [...instead, "editor_launch open starts one on the project"]);
|
|
53380
|
+
}
|
|
52695
53381
|
return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
|
|
52696
53382
|
...instead,
|
|
52697
53383
|
"Open the project in the editor with editor_launch, or by hand",
|
|
@@ -52840,7 +53526,8 @@ class GodotServer {
|
|
|
52840
53526
|
}
|
|
52841
53527
|
async handleRuntimeCommand(command, args, asked) {
|
|
52842
53528
|
const timeoutMs = asked.patience ?? this.runtimeTimeoutMs();
|
|
52843
|
-
const
|
|
53529
|
+
const callArgs = asParams(args);
|
|
53530
|
+
const { op: _op, projectPath, pid, outputPath, inline: _inline, ...params } = callArgs;
|
|
52844
53531
|
const waited = typeof pid === "number" ? null : await this.ownRunOnItsWay(typeof projectPath === "string" ? projectPath : this.ownProject, timeoutMs);
|
|
52845
53532
|
if (waited !== null) {
|
|
52846
53533
|
return this.createErrorResponse(waited, [
|
|
@@ -52873,6 +53560,13 @@ class GodotServer {
|
|
|
52873
53560
|
}
|
|
52874
53561
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
52875
53562
|
const saveTo = expectsScreenshot && typeof outputPath === "string" ? outputPath : null;
|
|
53563
|
+
const sendsThePicture = readBoolean(callArgs, "inline") !== false;
|
|
53564
|
+
if (expectsScreenshot && !sendsThePicture && saveTo === null) {
|
|
53565
|
+
return this.createErrorResponse("inline: false sends no picture back, so it needs outputPath to save one to; without it the capture would answer nothing to look at.", [
|
|
53566
|
+
"Pass outputPath, an absolute path ending in .png outside the project",
|
|
53567
|
+
"Or leave inline out to have the picture sent"
|
|
53568
|
+
]);
|
|
53569
|
+
}
|
|
52876
53570
|
if (saveTo !== null) {
|
|
52877
53571
|
const refusal = captureDestinationRefusal(saveTo, [
|
|
52878
53572
|
choice.endpoint.project.path,
|
|
@@ -52940,15 +53634,17 @@ class GodotServer {
|
|
|
52940
53634
|
}
|
|
52941
53635
|
}
|
|
52942
53636
|
const ranWith = [...onGame.alongside];
|
|
53637
|
+
const sent = sendsThePicture ? pictureToSend(screenshotPath, readString(payload, "jpegPath")) : null;
|
|
53638
|
+
const how = sent === null ? ". Not sent, as inline: false asked" : sent.mimeType === "image/jpeg" ? `. Sent as JPEG, ${kilobytes(sent.bytes)} against the PNG's ${kilobytes(sent.pngBytes)}${saveTo === null ? "" : "; the saved PNG is lossless"}` : "";
|
|
52943
53639
|
return {
|
|
52944
53640
|
content: [
|
|
52945
53641
|
{
|
|
52946
53642
|
type: "text",
|
|
52947
|
-
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${captureDetail(payload)}${saveTo === null ? "" : `. Saved to ${saveTo}`}${ranWith.length === 0 ? "" : `
|
|
53643
|
+
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${captureDetail(payload)}${saveTo === null ? "" : `. Saved to ${saveTo}`}${how}${ranWith.length === 0 ? "" : `
|
|
52948
53644
|
|
|
52949
53645
|
Ran alongside ${ranWith.join(", ")}. ${ALONGSIDE_NOTE}`}`
|
|
52950
53646
|
},
|
|
52951
|
-
{ type: "image", data:
|
|
53647
|
+
...sent === null ? [] : [{ type: "image", data: sent.data, mimeType: sent.mimeType }]
|
|
52952
53648
|
]
|
|
52953
53649
|
};
|
|
52954
53650
|
} finally {
|
|
@@ -53065,7 +53761,7 @@ async function runGodotServer() {
|
|
|
53065
53761
|
const server = new GodotServer;
|
|
53066
53762
|
await server.run();
|
|
53067
53763
|
}
|
|
53068
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
|
|
53764
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, MOST_UNDECLARED_ERRORS = 20, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
|
|
53069
53765
|
var init_server = __esm(() => {
|
|
53070
53766
|
init_dist();
|
|
53071
53767
|
init_stdio();
|
|
@@ -53123,6 +53819,8 @@ var init_server = __esm(() => {
|
|
|
53123
53819
|
init_server_version();
|
|
53124
53820
|
init_setup();
|
|
53125
53821
|
init_spawn();
|
|
53822
|
+
init_suite_judge();
|
|
53823
|
+
init_suite_plan();
|
|
53126
53824
|
init_test_selection();
|
|
53127
53825
|
init_tool_definitions();
|
|
53128
53826
|
init_tool_reference();
|
|
@@ -53172,6 +53870,7 @@ var init_server = __esm(() => {
|
|
|
53172
53870
|
});
|
|
53173
53871
|
PROJECT_INFO_SECTIONS2 = dictionary({
|
|
53174
53872
|
autoloads: { operation: "list_autoloads", params: () => ({}) },
|
|
53873
|
+
classes: { operation: "list_script_classes", params: () => ({}) },
|
|
53175
53874
|
input_actions: { operation: "list_input_actions", params: () => ({}) },
|
|
53176
53875
|
plugins: { operation: "list_plugins", params: () => ({}) },
|
|
53177
53876
|
export_presets: { operation: "list_export_presets", params: () => ({}) },
|
|
@@ -54184,10 +54883,10 @@ init_setup();
|
|
|
54184
54883
|
init_setup();
|
|
54185
54884
|
init_tool_definitions();
|
|
54186
54885
|
init_tool_reference();
|
|
54187
|
-
import { existsSync as
|
|
54188
|
-
import { basename as basename7, dirname as dirname9, join as
|
|
54886
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync5, readdirSync as readdirSync10, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
54887
|
+
import { basename as basename7, dirname as dirname9, join as join17 } from "node:path";
|
|
54189
54888
|
var SKILL_NAME = "gdharness";
|
|
54190
|
-
var SHARED_SKILLS =
|
|
54889
|
+
var SHARED_SKILLS = join17(".agents", "skills");
|
|
54191
54890
|
function skillMarkdown(version) {
|
|
54192
54891
|
return `---
|
|
54193
54892
|
name: ${SKILL_NAME}
|
|
@@ -54298,6 +54997,11 @@ engines would otherwise announce sixteen games that are not games. Set \`${SCRIP
|
|
|
54298
54997
|
true in \`project.godot\` to serve one anyway, which is for driving a \`-s\` script on purpose. Read
|
|
54299
54998
|
that setting rather than assuming the quiet: it is a default and not a property of script runs.
|
|
54300
54999
|
|
|
55000
|
+
A scene that is not the game, such as a bench, is run as a scene and is served like the game unless
|
|
55001
|
+
the project lists its folder in \`${QUIET_SCENES_SETTING}\`, a \`PackedStringArray\` of \`res://\`
|
|
55002
|
+
prefixes, empty by default. A run started on a scene under one of them neither serves nor
|
|
55003
|
+
announces, nor do the engines it starts, unless it was started to be profiled from its start.
|
|
55004
|
+
|
|
54301
55005
|
The autoload reaches an export. \`gdharness runtime off\` in the project before shipping.
|
|
54302
55006
|
|
|
54303
55007
|
## At a breakpoint
|
|
@@ -54419,27 +55123,27 @@ what they report while starting comes back under \`engine_messages_before_operat
|
|
|
54419
55123
|
function skillFiles(version) {
|
|
54420
55124
|
return new Map([
|
|
54421
55125
|
["SKILL.md", skillMarkdown(version)],
|
|
54422
|
-
[
|
|
55126
|
+
[join17("references", "tools.md"), renderToolsMarkdown()]
|
|
54423
55127
|
]);
|
|
54424
55128
|
}
|
|
54425
55129
|
function skillDirectories(harnesses, projectPath, exists) {
|
|
54426
|
-
const directories = [
|
|
55130
|
+
const directories = [join17(projectPath, SHARED_SKILLS, SKILL_NAME)];
|
|
54427
55131
|
for (const harness of harnesses) {
|
|
54428
55132
|
if (harness.skills === undefined) {
|
|
54429
55133
|
continue;
|
|
54430
55134
|
}
|
|
54431
|
-
const own =
|
|
54432
|
-
if ((!harness.skills.shared || exists(own)) && !directories.includes(
|
|
54433
|
-
directories.push(
|
|
55135
|
+
const own = join17(projectPath, harness.skills.dir);
|
|
55136
|
+
if ((!harness.skills.shared || exists(own)) && !directories.includes(join17(own, SKILL_NAME))) {
|
|
55137
|
+
directories.push(join17(own, SKILL_NAME));
|
|
54434
55138
|
}
|
|
54435
55139
|
}
|
|
54436
55140
|
return directories;
|
|
54437
55141
|
}
|
|
54438
55142
|
function everySkillDirectory(projectPath, harnesses) {
|
|
54439
|
-
const directories = new Set([
|
|
55143
|
+
const directories = new Set([join17(projectPath, SHARED_SKILLS, SKILL_NAME)]);
|
|
54440
55144
|
for (const harness of harnesses) {
|
|
54441
55145
|
if (harness.skills !== undefined) {
|
|
54442
|
-
directories.add(
|
|
55146
|
+
directories.add(join17(projectPath, harness.skills.dir, SKILL_NAME));
|
|
54443
55147
|
}
|
|
54444
55148
|
}
|
|
54445
55149
|
return [...directories];
|
|
@@ -54447,7 +55151,7 @@ function everySkillDirectory(projectPath, harnesses) {
|
|
|
54447
55151
|
function removeSkill(directories, projectPath) {
|
|
54448
55152
|
const removed = [];
|
|
54449
55153
|
for (const directory of directories) {
|
|
54450
|
-
if (
|
|
55154
|
+
if (existsSync15(directory)) {
|
|
54451
55155
|
rmSync6(directory, { recursive: true, force: true });
|
|
54452
55156
|
pruneEmpty(dirname9(directory), projectPath);
|
|
54453
55157
|
removed.push(directory);
|
|
@@ -54456,10 +55160,10 @@ function removeSkill(directories, projectPath) {
|
|
|
54456
55160
|
return removed;
|
|
54457
55161
|
}
|
|
54458
55162
|
function pruneEmpty(directory, projectPath) {
|
|
54459
|
-
const shared =
|
|
55163
|
+
const shared = join17(projectPath, ".agents");
|
|
54460
55164
|
let path = directory;
|
|
54461
55165
|
while (path !== projectPath && (basename7(path) === "skills" || path === shared)) {
|
|
54462
|
-
if (
|
|
55166
|
+
if (readdirSync10(path).length > 0) {
|
|
54463
55167
|
return;
|
|
54464
55168
|
}
|
|
54465
55169
|
rmSync6(path, { recursive: true, force: true });
|
|
@@ -54469,10 +55173,10 @@ function pruneEmpty(directory, projectPath) {
|
|
|
54469
55173
|
function writeSkill(directories, version) {
|
|
54470
55174
|
const files = skillFiles(version);
|
|
54471
55175
|
return directories.map((directory) => {
|
|
54472
|
-
const replaced =
|
|
55176
|
+
const replaced = existsSync15(directory);
|
|
54473
55177
|
rmSync6(directory, { recursive: true, force: true });
|
|
54474
55178
|
for (const [name, contents] of files) {
|
|
54475
|
-
const path =
|
|
55179
|
+
const path = join17(directory, name);
|
|
54476
55180
|
mkdirSync5(dirname9(path), { recursive: true });
|
|
54477
55181
|
writeFileSync7(path, contents, "utf8");
|
|
54478
55182
|
}
|
|
@@ -54485,27 +55189,27 @@ init_alive();
|
|
|
54485
55189
|
init_desktop();
|
|
54486
55190
|
import {
|
|
54487
55191
|
closeSync as closeSync3,
|
|
54488
|
-
existsSync as
|
|
55192
|
+
existsSync as existsSync19,
|
|
54489
55193
|
fstatSync,
|
|
54490
55194
|
mkdirSync as mkdirSync8,
|
|
54491
55195
|
openSync as openSync3,
|
|
54492
|
-
readdirSync as
|
|
54493
|
-
readFileSync as
|
|
55196
|
+
readdirSync as readdirSync12,
|
|
55197
|
+
readFileSync as readFileSync19,
|
|
54494
55198
|
readSync,
|
|
54495
55199
|
rmSync as rmSync10,
|
|
54496
|
-
statSync as
|
|
55200
|
+
statSync as statSync12,
|
|
54497
55201
|
writeFileSync as writeFileSync10
|
|
54498
55202
|
} from "node:fs";
|
|
54499
|
-
import { availableParallelism, freemem } from "node:os";
|
|
54500
|
-
import { isAbsolute as isAbsolute4, join as
|
|
55203
|
+
import { availableParallelism, freemem, totalmem } from "node:os";
|
|
55204
|
+
import { isAbsolute as isAbsolute4, join as join22, relative as relative3, resolve as resolve4, sep as sep2 } from "node:path";
|
|
54501
55205
|
import process12 from "node:process";
|
|
54502
55206
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
54503
55207
|
|
|
54504
55208
|
// src/engine-slots.ts
|
|
54505
55209
|
init_alive();
|
|
54506
55210
|
init_process_children();
|
|
54507
|
-
import { closeSync as closeSync2, mkdirSync as mkdirSync6, openSync as openSync2, readFileSync as
|
|
54508
|
-
import { join as
|
|
55211
|
+
import { closeSync as closeSync2, mkdirSync as mkdirSync6, openSync as openSync2, readFileSync as readFileSync14, rmSync as rmSync8, statSync as statSync10, writeSync as writeSync2 } from "node:fs";
|
|
55212
|
+
import { join as join19 } from "node:path";
|
|
54509
55213
|
import process11 from "node:process";
|
|
54510
55214
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
54511
55215
|
var START_TOLERANCE_MS = 2000;
|
|
@@ -54556,7 +55260,7 @@ class EngineSlots {
|
|
|
54556
55260
|
for (;; ) {
|
|
54557
55261
|
signal?.throwIfAborted();
|
|
54558
55262
|
for (let slot = 0;slot < this.count; slot += 1) {
|
|
54559
|
-
const lock =
|
|
55263
|
+
const lock = join19(this.directory, `${String(slot)}.lock`);
|
|
54560
55264
|
if (this.made(lock) || await this.takenOver(lock)) {
|
|
54561
55265
|
this.held.add(lock);
|
|
54562
55266
|
return lock;
|
|
@@ -54573,7 +55277,7 @@ class EngineSlots {
|
|
|
54573
55277
|
release(lock) {
|
|
54574
55278
|
this.held.delete(lock);
|
|
54575
55279
|
try {
|
|
54576
|
-
if (holderOf(
|
|
55280
|
+
if (holderOf(readFileSync14(lock, "utf8"))?.pid === this.holder.pid) {
|
|
54577
55281
|
rmSync8(lock, { force: true });
|
|
54578
55282
|
}
|
|
54579
55283
|
} catch {}
|
|
@@ -54599,7 +55303,7 @@ class EngineSlots {
|
|
|
54599
55303
|
const steal = `${lock}.steal`;
|
|
54600
55304
|
if (!this.made(steal)) {
|
|
54601
55305
|
try {
|
|
54602
|
-
if (Date.now() -
|
|
55306
|
+
if (Date.now() - statSync10(steal).mtimeMs > UNWRITTEN_MS) {
|
|
54603
55307
|
rmSync8(steal, { force: true });
|
|
54604
55308
|
}
|
|
54605
55309
|
} catch {}
|
|
@@ -54623,8 +55327,8 @@ class EngineSlots {
|
|
|
54623
55327
|
let content;
|
|
54624
55328
|
let writtenAt;
|
|
54625
55329
|
try {
|
|
54626
|
-
content =
|
|
54627
|
-
writtenAt =
|
|
55330
|
+
content = readFileSync14(lock, "utf8");
|
|
55331
|
+
writtenAt = statSync10(lock).mtimeMs;
|
|
54628
55332
|
} catch {
|
|
54629
55333
|
return false;
|
|
54630
55334
|
}
|
|
@@ -54651,9 +55355,9 @@ init_spawn();
|
|
|
54651
55355
|
|
|
54652
55356
|
// src/suite-fingerprint.ts
|
|
54653
55357
|
init_class_cache();
|
|
54654
|
-
import { createHash as
|
|
54655
|
-
import { existsSync as
|
|
54656
|
-
import { join as
|
|
55358
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
55359
|
+
import { existsSync as existsSync17, readdirSync as readdirSync11, readFileSync as readFileSync15, statSync as statSync11 } from "node:fs";
|
|
55360
|
+
import { join as join20, posix as posix2 } from "node:path";
|
|
54657
55361
|
var RES_LITERAL = /res:\/\/[^"'\s)\]<>]*/g;
|
|
54658
55362
|
var RELATIVE_LOAD = /\b(?:pre)?load\(\s*["']((?!res:\/\/|uid:\/\/)[^"']+)["']/g;
|
|
54659
55363
|
var REACHES_AT_RUN_TIME = [
|
|
@@ -54673,7 +55377,7 @@ class SuiteFingerprints {
|
|
|
54673
55377
|
constructor(scripts, always) {
|
|
54674
55378
|
this.scripts = scripts;
|
|
54675
55379
|
this.everywhere = ["res://project.godot", ...extensionsOf(scripts.projectPath)];
|
|
54676
|
-
this.alwaysDigest =
|
|
55380
|
+
this.alwaysDigest = createHash6("sha256").update(always.join(`
|
|
54677
55381
|
`)).digest("hex");
|
|
54678
55382
|
}
|
|
54679
55383
|
of(suite) {
|
|
@@ -54690,7 +55394,7 @@ class SuiteFingerprints {
|
|
|
54690
55394
|
}
|
|
54691
55395
|
pending.push(...reached.reaches);
|
|
54692
55396
|
}
|
|
54693
|
-
const hash =
|
|
55397
|
+
const hash = createHash6("sha256").update(this.alwaysDigest);
|
|
54694
55398
|
for (const path of [...seen].sort()) {
|
|
54695
55399
|
hash.update(`${path}
|
|
54696
55400
|
${this.read(path).digest}
|
|
@@ -54709,20 +55413,20 @@ ${this.read(path).digest}
|
|
|
54709
55413
|
return reached;
|
|
54710
55414
|
}
|
|
54711
55415
|
addon(root) {
|
|
54712
|
-
const hash =
|
|
55416
|
+
const hash = createHash6("sha256");
|
|
54713
55417
|
const walk = (directory, under) => {
|
|
54714
|
-
for (const entry of
|
|
54715
|
-
const file =
|
|
55418
|
+
for (const entry of readdirSync11(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
55419
|
+
const file = join20(directory, entry.name);
|
|
54716
55420
|
if (entry.isDirectory()) {
|
|
54717
55421
|
walk(file, `${under}${entry.name}/`);
|
|
54718
55422
|
} else if (entry.isFile()) {
|
|
54719
55423
|
hash.update(`${under}${entry.name}
|
|
54720
|
-
`).update(
|
|
55424
|
+
`).update(readFileSync15(file));
|
|
54721
55425
|
}
|
|
54722
55426
|
}
|
|
54723
55427
|
};
|
|
54724
|
-
const directory =
|
|
54725
|
-
if (
|
|
55428
|
+
const directory = join20(this.scripts.projectPath, root.slice("res://".length));
|
|
55429
|
+
if (existsSync17(directory)) {
|
|
54726
55430
|
walk(directory, "");
|
|
54727
55431
|
}
|
|
54728
55432
|
return { digest: hash.digest("hex"), reaches: [], unreadable: null };
|
|
@@ -54731,26 +55435,26 @@ ${this.read(path).digest}
|
|
|
54731
55435
|
if (PLACEHOLDER.test(path)) {
|
|
54732
55436
|
return { digest: "", reaches: [], unreadable: `${path} is filled in at run time` };
|
|
54733
55437
|
}
|
|
54734
|
-
const file =
|
|
54735
|
-
if (!
|
|
55438
|
+
const file = join20(this.scripts.projectPath, path.slice("res://".length));
|
|
55439
|
+
if (!existsSync17(file)) {
|
|
54736
55440
|
return { digest: "missing", reaches: [], unreadable: null };
|
|
54737
55441
|
}
|
|
54738
|
-
if (
|
|
54739
|
-
const inside =
|
|
55442
|
+
if (statSync11(file).isDirectory()) {
|
|
55443
|
+
const inside = readdirSync11(file, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".")).map((entry) => `${path.endsWith("/") ? path : `${path}/`}${entry.name}`).sort();
|
|
54740
55444
|
return { digest: inside.join(`
|
|
54741
55445
|
`), reaches: inside, unreadable: null };
|
|
54742
55446
|
}
|
|
54743
55447
|
if (BINARY_RESOURCE.test(path)) {
|
|
54744
55448
|
return { digest: "", reaches: [], unreadable: `${path} is a binary resource` };
|
|
54745
55449
|
}
|
|
54746
|
-
const bytes =
|
|
54747
|
-
const digest =
|
|
55450
|
+
const bytes = readFileSync15(file);
|
|
55451
|
+
const digest = createHash6("sha256").update(bytes).digest("hex");
|
|
54748
55452
|
const text = bytes.toString("latin1");
|
|
54749
55453
|
const reaches = new Set;
|
|
54750
55454
|
for (const [literal] of text.matchAll(RES_LITERAL)) {
|
|
54751
55455
|
reaches.add(literal);
|
|
54752
55456
|
}
|
|
54753
|
-
if (
|
|
55457
|
+
if (existsSync17(`${file}.import`)) {
|
|
54754
55458
|
reaches.add(`${path}.import`);
|
|
54755
55459
|
}
|
|
54756
55460
|
if (path.endsWith(".gd")) {
|
|
@@ -54780,532 +55484,28 @@ ${this.read(path).digest}
|
|
|
54780
55484
|
}
|
|
54781
55485
|
}
|
|
54782
55486
|
function extensionsOf(projectPath) {
|
|
54783
|
-
const list =
|
|
54784
|
-
return
|
|
55487
|
+
const list = join20(projectPath, ".godot", "extension_list.cfg");
|
|
55488
|
+
return existsSync17(list) ? readFileSync15(list, "utf8").split(`
|
|
54785
55489
|
`).map((line) => line.trim()).filter((line) => line.startsWith("res://")) : [];
|
|
54786
55490
|
}
|
|
54787
55491
|
|
|
54788
|
-
// src/suite-
|
|
54789
|
-
|
|
54790
|
-
|
|
54791
|
-
var SUITE_OPENED = /^Run Test Suite:\s*(res:\/\/.+?\.gd)\s*$/;
|
|
54792
|
-
var TIME = String.raw`(?:\d+h )?(?:\d+min )?(?:\d+s )?\d+ms`;
|
|
54793
|
-
var CASE_LINE = new RegExp(String.raw`^\s*(res://.+?\.gd) > (.+?) (STARTED|PASSED|FAILED|FLAKY|SKIPPED|WARNING)(?: \((?:retry \d+|\d+ retries)\))?(?: (${TIME}))?\s*$`);
|
|
54794
|
-
var STATISTICS = new RegExp(String.raw`^Statistics:.*\|\s*([A-Z]+)(?:\s+\(.*?\))?\s+(${TIME})\s*$`);
|
|
54795
|
-
var ERROR_LINE = /^\s*(?:SCRIPT |USER )?ERROR: (.*?)\s*$/;
|
|
54796
|
-
var EXECUTED = /^Executed test cases\s*:\s*\((\d+)\//;
|
|
54797
|
-
var DRIVER_FALLBACK = /^\s*WARNING: Your video card drivers .*, switching to /;
|
|
54798
|
-
var LEAK = /ObjectDB instances (?:were )?leaked at exit|resources? still in use at exit|Pages in use exist at exit/;
|
|
54799
|
-
var STARVED = [
|
|
54800
|
-
/Parameter "mem(?:_new)?" is null\.?\s*\n\s*at: \w+ \((?:\.\/)?core\/(?:os\/memory\.cpp|templates\/cowdata\.h):/g,
|
|
54801
|
-
/ERROR: Failed decoding WebP image\.\s*\n\s*at: _webp_unpack/g
|
|
54802
|
-
];
|
|
54803
|
-
function secondsOf(time) {
|
|
54804
|
-
let ms = 0;
|
|
54805
|
-
for (const [, amount, unit] of time.matchAll(/(\d+)(h|min|ms|s)\b/g)) {
|
|
54806
|
-
ms += Number(amount) * (unit === "h" ? 3600000 : unit === "min" ? 60000 : unit === "s" ? 1000 : 1);
|
|
54807
|
-
}
|
|
54808
|
-
return ms / 1000;
|
|
54809
|
-
}
|
|
54810
|
-
function funcOf(name) {
|
|
54811
|
-
const colon = name.indexOf(":");
|
|
54812
|
-
return colon < 0 ? name : name.slice(0, colon);
|
|
54813
|
-
}
|
|
54814
|
-
function readRun(printed) {
|
|
54815
|
-
const lines = stripVTControlCharacters(printed).replaceAll(`\r
|
|
54816
|
-
`, `
|
|
54817
|
-
`).split(`
|
|
54818
|
-
`);
|
|
54819
|
-
const suites = new Map;
|
|
54820
|
-
const errors = [];
|
|
54821
|
-
const leaks = [];
|
|
54822
|
-
let executed = null;
|
|
54823
|
-
let open = null;
|
|
54824
|
-
let during = null;
|
|
54825
|
-
let afterCase = [];
|
|
54826
|
-
let lastCase = null;
|
|
54827
|
-
const settleAfterCase = (owner) => {
|
|
54828
|
-
for (const held of afterCase) {
|
|
54829
|
-
errors.push({ ...held, suite: open?.path ?? null, during: owner });
|
|
54830
|
-
}
|
|
54831
|
-
afterCase = [];
|
|
54832
|
-
};
|
|
54833
|
-
for (const [at, line] of lines.entries()) {
|
|
54834
|
-
const opened = SUITE_OPENED.exec(line);
|
|
54835
|
-
if (opened?.[1] !== undefined) {
|
|
54836
|
-
settleAfterCase("after");
|
|
54837
|
-
open = suites.get(opened[1]) ?? { path: opened[1], cases: [], closed: null };
|
|
54838
|
-
suites.set(open.path, open);
|
|
54839
|
-
during = "before";
|
|
54840
|
-
lastCase = null;
|
|
54841
|
-
continue;
|
|
54842
|
-
}
|
|
54843
|
-
const caseLine = CASE_LINE.exec(line);
|
|
54844
|
-
if (caseLine !== null && open !== null) {
|
|
54845
|
-
const name = caseLine[2] ?? "";
|
|
54846
|
-
if (caseLine[3] === "STARTED") {
|
|
54847
|
-
settleAfterCase(lastCase ?? "before");
|
|
54848
|
-
during = name;
|
|
54849
|
-
} else {
|
|
54850
|
-
open.cases.push({
|
|
54851
|
-
name,
|
|
54852
|
-
func: funcOf(name),
|
|
54853
|
-
verdict: caseLine[3] ?? "",
|
|
54854
|
-
seconds: secondsOf(caseLine[4] ?? ""),
|
|
54855
|
-
at
|
|
54856
|
-
});
|
|
54857
|
-
lastCase = name;
|
|
54858
|
-
during = null;
|
|
54859
|
-
}
|
|
54860
|
-
continue;
|
|
54861
|
-
}
|
|
54862
|
-
const statistics = STATISTICS.exec(line);
|
|
54863
|
-
if (statistics !== null && open !== null) {
|
|
54864
|
-
settleAfterCase("after");
|
|
54865
|
-
open.closed = { verdict: statistics[1] ?? "", seconds: secondsOf(statistics[2] ?? "") };
|
|
54866
|
-
open = null;
|
|
54867
|
-
during = null;
|
|
54868
|
-
lastCase = null;
|
|
54869
|
-
continue;
|
|
54870
|
-
}
|
|
54871
|
-
const counted = EXECUTED.exec(line);
|
|
54872
|
-
if (counted?.[1] !== undefined) {
|
|
54873
|
-
executed = Number(counted[1]);
|
|
54874
|
-
continue;
|
|
54875
|
-
}
|
|
54876
|
-
if (LEAK.test(line)) {
|
|
54877
|
-
leaks.push(line.trim());
|
|
54878
|
-
continue;
|
|
54879
|
-
}
|
|
54880
|
-
if (suites.size === 0 && DRIVER_FALLBACK.test(line)) {
|
|
54881
|
-
errors.length = 0;
|
|
54882
|
-
continue;
|
|
54883
|
-
}
|
|
54884
|
-
const error = ERROR_LINE.exec(line);
|
|
54885
|
-
if (error === null) {
|
|
54886
|
-
continue;
|
|
54887
|
-
}
|
|
54888
|
-
const text = error[1] ?? "";
|
|
54889
|
-
if (open === null) {
|
|
54890
|
-
errors.push({ text, at, suite: null, during: null });
|
|
54891
|
-
} else if (during === null) {
|
|
54892
|
-
afterCase.push({ text, at });
|
|
54893
|
-
} else {
|
|
54894
|
-
errors.push({ text, at, suite: open.path, during });
|
|
54895
|
-
}
|
|
54896
|
-
}
|
|
54897
|
-
const last = open;
|
|
54898
|
-
const stoppedIn = last === null ? null : during === null ? `${last.path}, after ${lastCase ?? "its cases"}` : during === "before" ? `${last.path}, in before` : `${last.path} > ${during}`;
|
|
54899
|
-
settleAfterCase("after");
|
|
54900
|
-
const joined = lines.join(`
|
|
54901
|
-
`);
|
|
54902
|
-
const starved = STARVED.flatMap((pattern) => [...joined.matchAll(pattern)].map((found) => found[0]));
|
|
54903
|
-
const unloadable = scriptErrorsPrinted(lines).map((error) => `${error.path}${error.line === null ? "" : `:${String(error.line)}`}: ${error.message}`);
|
|
54904
|
-
return { lines, suites, errors, leaks, starved, executed, unloadable, stoppedIn };
|
|
54905
|
-
}
|
|
54906
|
-
var WINDOWS_EXITS = new Map([
|
|
54907
|
-
[3221225477, "STATUS_ACCESS_VIOLATION"],
|
|
54908
|
-
[3221225495, "STATUS_NO_MEMORY"],
|
|
54909
|
-
[3221225773, "STATUS_COMMITMENT_LIMIT"],
|
|
54910
|
-
[3221225794, "STATUS_DLL_INIT_FAILED"],
|
|
54911
|
-
[3221226505, "STATUS_STACK_BUFFER_OVERRUN"],
|
|
54912
|
-
[3228369022, "a module that could not be found (VcppException)"],
|
|
54913
|
-
[3228369023, "a procedure that could not be found (VcppException)"]
|
|
54914
|
-
]);
|
|
54915
|
-
var MEMORY_EXITS = new Set([3221225495, 3221225773]);
|
|
54916
|
-
function exitNamed(code) {
|
|
54917
|
-
const unsigned = code >>> 0;
|
|
54918
|
-
const name = WINDOWS_EXITS.get(unsigned);
|
|
54919
|
-
return name === undefined ? String(code) : `0x${unsigned.toString(16).toUpperCase()} (${name})`;
|
|
54920
|
-
}
|
|
54921
|
-
var TAIL = 60;
|
|
54922
|
-
var MOST_NAMED = 8;
|
|
54923
|
-
function named(items) {
|
|
54924
|
-
const shown = items.slice(0, MOST_NAMED).join(", ");
|
|
54925
|
-
return items.length > MOST_NAMED ? `${shown} and ${String(items.length - MOST_NAMED)} more` : shown;
|
|
54926
|
-
}
|
|
54927
|
-
function judgeEngine(printed, handed, end) {
|
|
54928
|
-
const faults = [];
|
|
54929
|
-
const failing = new Set;
|
|
54930
|
-
const shownAt = new Set;
|
|
54931
|
-
if (end.ended !== null) {
|
|
54932
|
-
faults.push(end.ended);
|
|
54933
|
-
} else if (end.code !== 0) {
|
|
54934
|
-
faults.push(end.code === null ? `the engine was ended by ${end.signal ?? "a signal"}` : `the engine exited ${exitNamed(end.code)}`);
|
|
54935
|
-
}
|
|
54936
|
-
if (printed.unloadable.length > 0) {
|
|
54937
|
-
faults.push(`gdUnit4 could not load ${named(printed.unloadable)}, so it ran nothing`);
|
|
54938
|
-
}
|
|
54939
|
-
if (printed.executed === null || printed.executed === 0) {
|
|
54940
|
-
faults.push(printed.executed === null ? "the run never printed its summary" : "the run executed no test case at all, whatever it exited with");
|
|
54941
|
-
}
|
|
54942
|
-
for (const { path, cases } of handed.suites) {
|
|
54943
|
-
const suite = printed.suites.get(path);
|
|
54944
|
-
if (suite === undefined) {
|
|
54945
|
-
faults.push(`${path} never started`);
|
|
54946
|
-
failing.add(path);
|
|
54947
|
-
continue;
|
|
54948
|
-
}
|
|
54949
|
-
if (suite.closed === null) {
|
|
54950
|
-
faults.push(`${path} never finished`);
|
|
54951
|
-
failing.add(path);
|
|
54952
|
-
} else if (suite.closed.verdict !== "PASSED") {
|
|
54953
|
-
if (suite.cases.every((ran) => ran.verdict !== "FAILED")) {
|
|
54954
|
-
faults.push(`${path} reported ${suite.closed.verdict} with every case passing, so a hook failed`);
|
|
54955
|
-
}
|
|
54956
|
-
failing.add(path);
|
|
54957
|
-
}
|
|
54958
|
-
const finished = new Set(suite.cases.map((ran) => ran.func));
|
|
54959
|
-
const missing = cases.filter((func) => !finished.has(func));
|
|
54960
|
-
if (missing.length > 0 && suite.closed !== null) {
|
|
54961
|
-
faults.push(`${path} closed without running ${named(missing)}`);
|
|
54962
|
-
failing.add(path);
|
|
54963
|
-
}
|
|
54964
|
-
const wanted = new Set(cases);
|
|
54965
|
-
const unasked = [...finished].filter((func) => !wanted.has(func));
|
|
54966
|
-
if (unasked.length > 0) {
|
|
54967
|
-
faults.push(`${path} ran ${named(unasked)}, which gdharness did not read as cases of it to run here, so its ` + "plan of the cases was wrong");
|
|
54968
|
-
failing.add(path);
|
|
54969
|
-
}
|
|
54970
|
-
for (const ran of suite.cases) {
|
|
54971
|
-
if (ran.verdict === "FAILED" || ran.verdict === "FLAKY") {
|
|
54972
|
-
faults.push(ran.verdict === "FLAKY" ? `${path} > ${ran.name} failed before it passed on a retry` : `${path} > ${ran.name} failed`);
|
|
54973
|
-
failing.add(path);
|
|
54974
|
-
for (let at = ran.at;at < printed.lines.length && (at === ran.at || printed.lines[at]?.trim()); at += 1) {
|
|
54975
|
-
shownAt.add(at);
|
|
54976
|
-
}
|
|
54977
|
-
}
|
|
54978
|
-
}
|
|
54979
|
-
}
|
|
54980
|
-
const unexpected = unexpectedErrors(printed, handed);
|
|
54981
|
-
for (const error of unexpected.printed) {
|
|
54982
|
-
const where = error.suite === null ? "outside every suite" : `in ${error.suite} > ${error.during ?? ""}`;
|
|
54983
|
-
faults.push(`an engine error ${where}: ${error.text}`);
|
|
54984
|
-
if (error.suite !== null) {
|
|
54985
|
-
failing.add(error.suite);
|
|
54986
|
-
}
|
|
54987
|
-
shownAt.add(error.at);
|
|
54988
|
-
shownAt.add(error.at + 1);
|
|
54989
|
-
}
|
|
54990
|
-
for (const missing of unexpected.neverPrinted) {
|
|
54991
|
-
faults.push(`${missing.suite} > ${missing.during} declares the error "${missing.text}", which it never printed`);
|
|
54992
|
-
failing.add(missing.suite);
|
|
54993
|
-
}
|
|
54994
|
-
for (const leak of printed.leaks) {
|
|
54995
|
-
faults.push(`the engine exited holding what it should have freed: ${leak}`);
|
|
54996
|
-
}
|
|
54997
|
-
const memoryExit = end.code !== null && MEMORY_EXITS.has(end.code >>> 0);
|
|
54998
|
-
const starved = memoryExit || printed.starved.length > 0;
|
|
54999
|
-
if (printed.starved.length > 0) {
|
|
55000
|
-
faults.push("the engine ran out of memory");
|
|
55001
|
-
}
|
|
55002
|
-
printed.lines.forEach((line, at) => {
|
|
55003
|
-
if (/^(?:Overall Summary|Executed test|Total execution time| ?Exit code)/.test(line.trim())) {
|
|
55004
|
-
shownAt.add(at);
|
|
55005
|
-
}
|
|
55006
|
-
});
|
|
55007
|
-
const passed = faults.length === 0;
|
|
55008
|
-
const passedSuites = handed.suites.map((suite) => suite.path).filter((path) => !failing.has(path));
|
|
55009
|
-
const cutShort = end.ended !== null || printed.executed === null || handed.suites.some(({ path }) => printed.suites.get(path)?.closed == null);
|
|
55010
|
-
return {
|
|
55011
|
-
passed,
|
|
55012
|
-
faults,
|
|
55013
|
-
passedSuites,
|
|
55014
|
-
starved,
|
|
55015
|
-
shown: passed ? [] : linesShown(printed.lines, shownAt, cutShort)
|
|
55016
|
-
};
|
|
55017
|
-
}
|
|
55018
|
-
function linesShown(lines, picked, cutShort) {
|
|
55019
|
-
const summary = [...picked].filter((at) => at < lines.length).sort((a, b) => a - b);
|
|
55020
|
-
const telling = summary.some((at) => !/^(?:Overall Summary|Executed test|Total execution|\s*Exit code)/.test(lines[at]?.trim() ?? ""));
|
|
55021
|
-
if (!telling && !cutShort) {
|
|
55022
|
-
return [];
|
|
55023
|
-
}
|
|
55024
|
-
if (!telling) {
|
|
55025
|
-
return lines.slice(Math.max(0, lines.length - TAIL)).filter((line, at, all) => at < all.length - 1 || line !== "");
|
|
55026
|
-
}
|
|
55027
|
-
const shown = [];
|
|
55028
|
-
let previous = -1;
|
|
55029
|
-
for (const at of summary) {
|
|
55030
|
-
if (previous >= 0 && at > previous + 1) {
|
|
55031
|
-
shown.push("...");
|
|
55032
|
-
}
|
|
55033
|
-
shown.push(lines[at] ?? "");
|
|
55034
|
-
previous = at;
|
|
55035
|
-
}
|
|
55036
|
-
return shown;
|
|
55037
|
-
}
|
|
55038
|
-
function unexpectedErrors(printed, handed) {
|
|
55039
|
-
const owed = new Map;
|
|
55040
|
-
for (const { path } of handed.suites) {
|
|
55041
|
-
const suite = printed.suites.get(path);
|
|
55042
|
-
const declared = handed.expected[path];
|
|
55043
|
-
if (suite === undefined || declared === undefined) {
|
|
55044
|
-
continue;
|
|
55045
|
-
}
|
|
55046
|
-
const ranHooks = suite.closed === null ? ["before"] : ["before", "after"];
|
|
55047
|
-
for (const during of [...ranHooks, ...suite.cases.map((ran) => ran.name)]) {
|
|
55048
|
-
const texts = declared[funcOf(during)];
|
|
55049
|
-
if (Array.isArray(texts) && texts.length > 0) {
|
|
55050
|
-
owed.set(`${path}
|
|
55051
|
-
${during}`, {
|
|
55052
|
-
suite: path,
|
|
55053
|
-
during,
|
|
55054
|
-
texts: texts.map((text) => String(text))
|
|
55055
|
-
});
|
|
55056
|
-
}
|
|
55057
|
-
}
|
|
55058
|
-
}
|
|
55059
|
-
const unexpected = [];
|
|
55060
|
-
for (const error of printed.errors) {
|
|
55061
|
-
const debt = error.suite === null || error.during === null ? undefined : owed.get(`${error.suite}
|
|
55062
|
-
${error.during}`);
|
|
55063
|
-
const index = debt?.texts.indexOf(error.text) ?? -1;
|
|
55064
|
-
if (debt === undefined || index < 0) {
|
|
55065
|
-
unexpected.push(error);
|
|
55066
|
-
} else {
|
|
55067
|
-
debt.texts.splice(index, 1);
|
|
55068
|
-
}
|
|
55069
|
-
}
|
|
55070
|
-
const neverPrinted = [...owed.values()].flatMap((debt) => debt.texts.map((text) => ({ suite: debt.suite, during: debt.during, text })));
|
|
55071
|
-
return { printed: unexpected, neverPrinted };
|
|
55072
|
-
}
|
|
55073
|
-
|
|
55074
|
-
// src/suite-plan.ts
|
|
55075
|
-
init_class_cache();
|
|
55076
|
-
init_gdscript_source();
|
|
55077
|
-
init_whole_file();
|
|
55078
|
-
import { existsSync as existsSync17, mkdirSync as mkdirSync7, readFileSync as readFileSync15 } from "node:fs";
|
|
55079
|
-
import { dirname as dirname10, posix as posix3 } from "node:path";
|
|
55080
|
-
var SUITE_BASE = "GdUnitTestSuite";
|
|
55081
|
-
var CASE_PREFIX = "test_";
|
|
55082
|
-
var NEEDS_WINDOW = /^const\s+GDHARNESS_NEEDS_WINDOW\b[^=\n]*=\s*true\b/m;
|
|
55083
|
-
var SCENE_RUNNER = /\bscene_runner\s*\(/;
|
|
55084
|
-
|
|
55085
|
-
class ProjectScripts {
|
|
55086
|
-
sources = new Map;
|
|
55087
|
-
classes = new Map;
|
|
55088
|
-
read = new Map;
|
|
55089
|
-
projectPath;
|
|
55090
|
-
constructor(projectPath) {
|
|
55091
|
-
this.projectPath = projectPath;
|
|
55092
|
-
eachScript(projectPath, (script, source) => {
|
|
55093
|
-
this.sources.set(script, source);
|
|
55094
|
-
const declared = declaredClassOf(source);
|
|
55095
|
-
if (declared !== null) {
|
|
55096
|
-
this.classes.set(declared, script);
|
|
55097
|
-
}
|
|
55098
|
-
});
|
|
55099
|
-
}
|
|
55100
|
-
script(path) {
|
|
55101
|
-
const known = this.read.get(path);
|
|
55102
|
-
if (known !== undefined) {
|
|
55103
|
-
return known;
|
|
55104
|
-
}
|
|
55105
|
-
const source = this.sources.get(path);
|
|
55106
|
-
if (source === undefined) {
|
|
55107
|
-
return;
|
|
55108
|
-
}
|
|
55109
|
-
const shape = shapeOf(source);
|
|
55110
|
-
const read = {
|
|
55111
|
-
source,
|
|
55112
|
-
extendsAs: shape.body.extendsAs,
|
|
55113
|
-
funcs: shape.body.declarations.filter((d) => d.kind === "func").map((d) => d.name),
|
|
55114
|
-
code: codeOf(source)
|
|
55115
|
-
};
|
|
55116
|
-
this.read.set(path, read);
|
|
55117
|
-
return read;
|
|
55118
|
-
}
|
|
55119
|
-
base(path) {
|
|
55120
|
-
const extendsAs = this.script(path)?.extendsAs ?? null;
|
|
55121
|
-
if (extendsAs === null) {
|
|
55122
|
-
return null;
|
|
55123
|
-
}
|
|
55124
|
-
const quote = extendsAs[0];
|
|
55125
|
-
if (quote === '"' || quote === "'") {
|
|
55126
|
-
const target = extendsAs.slice(1, -1);
|
|
55127
|
-
return {
|
|
55128
|
-
path: target.startsWith("res://") ? target : `res://${posix3.join(posix3.dirname(path.slice(6)), target)}`
|
|
55129
|
-
};
|
|
55130
|
-
}
|
|
55131
|
-
const name = extendsAs.split(".")[0] ?? extendsAs;
|
|
55132
|
-
const declaring = this.classes.get(name);
|
|
55133
|
-
return declaring === undefined ? { name } : { path: declaring };
|
|
55134
|
-
}
|
|
55135
|
-
suite(path) {
|
|
55136
|
-
const cases = new Set;
|
|
55137
|
-
let windowed = false;
|
|
55138
|
-
const seen = new Set;
|
|
55139
|
-
let at = path;
|
|
55140
|
-
while (!seen.has(at)) {
|
|
55141
|
-
seen.add(at);
|
|
55142
|
-
const read = this.script(at);
|
|
55143
|
-
if (read === undefined) {
|
|
55144
|
-
return null;
|
|
55145
|
-
}
|
|
55146
|
-
for (const func of read.funcs.filter((name) => name.startsWith(CASE_PREFIX))) {
|
|
55147
|
-
cases.add(func);
|
|
55148
|
-
}
|
|
55149
|
-
windowed ||= NEEDS_WINDOW.test(read.code) || SCENE_RUNNER.test(read.code);
|
|
55150
|
-
const base = this.base(at);
|
|
55151
|
-
if (base === null) {
|
|
55152
|
-
return null;
|
|
55153
|
-
}
|
|
55154
|
-
if ("name" in base ? base.name === SUITE_BASE : base.path === this.classes.get(SUITE_BASE)) {
|
|
55155
|
-
return cases.size > 0 ? { path, cases: [...cases].sort(), windowed } : null;
|
|
55156
|
-
}
|
|
55157
|
-
if ("name" in base) {
|
|
55158
|
-
return null;
|
|
55159
|
-
}
|
|
55160
|
-
at = base.path;
|
|
55161
|
-
}
|
|
55162
|
-
return null;
|
|
55163
|
-
}
|
|
55164
|
-
suitesUnder(roots) {
|
|
55165
|
-
const prefixes = roots.map((root) => root.endsWith("/") ? root : `${root}/`);
|
|
55166
|
-
return [...this.sources.keys()].filter((path) => prefixes.some((prefix) => path.startsWith(prefix))).sort().flatMap((path) => this.suite(path) ?? []);
|
|
55167
|
-
}
|
|
55168
|
-
}
|
|
55169
|
-
function readTimes(file) {
|
|
55170
|
-
try {
|
|
55171
|
-
const parsed = JSON.parse(readFileSync15(file, "utf8"));
|
|
55172
|
-
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
55173
|
-
} catch {
|
|
55174
|
-
return {};
|
|
55175
|
-
}
|
|
55176
|
-
}
|
|
55177
|
-
function writeJson(file, value) {
|
|
55178
|
-
mkdirSync7(dirname10(file), { recursive: true });
|
|
55179
|
-
writeWhole(file, `${JSON.stringify(value, null, 1)}
|
|
55180
|
-
`);
|
|
55181
|
-
}
|
|
55182
|
-
function timesAfter(times, suites, runs) {
|
|
55183
|
-
const next = { ...times };
|
|
55184
|
-
const declared = new Map(suites.map((suite) => [suite.path, suite]));
|
|
55185
|
-
const measured = new Map;
|
|
55186
|
-
for (const run of runs) {
|
|
55187
|
-
for (const printed of run.suites.values()) {
|
|
55188
|
-
if (printed.closed === null || !declared.has(printed.path)) {
|
|
55189
|
-
continue;
|
|
55190
|
-
}
|
|
55191
|
-
const entry = measured.get(printed.path) ?? { cases: new Map, around: 0 };
|
|
55192
|
-
const own = new Map;
|
|
55193
|
-
for (const ran of printed.cases) {
|
|
55194
|
-
own.set(ran.func, (own.get(ran.func) ?? 0) + ran.seconds);
|
|
55195
|
-
}
|
|
55196
|
-
const inCases = [...own.values()].reduce((sum, seconds) => sum + seconds, 0);
|
|
55197
|
-
entry.around = Math.max(entry.around, printed.closed.seconds - inCases);
|
|
55198
|
-
for (const [func, seconds] of own) {
|
|
55199
|
-
entry.cases.set(func, seconds);
|
|
55200
|
-
}
|
|
55201
|
-
measured.set(printed.path, entry);
|
|
55202
|
-
}
|
|
55203
|
-
}
|
|
55204
|
-
for (const [path, entry] of measured) {
|
|
55205
|
-
const suite = declared.get(path);
|
|
55206
|
-
const cases = {};
|
|
55207
|
-
for (const func of suite?.cases ?? []) {
|
|
55208
|
-
const seconds = entry.cases.get(func) ?? times[path]?.cases[func];
|
|
55209
|
-
if (seconds !== undefined) {
|
|
55210
|
-
cases[func] = round(seconds);
|
|
55211
|
-
}
|
|
55212
|
-
}
|
|
55213
|
-
const inCases = Object.values(cases).reduce((sum, seconds) => sum + seconds, 0);
|
|
55214
|
-
next[path] = { seconds: round(entry.around + inCases), cases };
|
|
55215
|
-
}
|
|
55216
|
-
return next;
|
|
55217
|
-
}
|
|
55218
|
-
function round(seconds) {
|
|
55219
|
-
return Math.round(seconds * 1000) / 1000;
|
|
55220
|
-
}
|
|
55221
|
-
var UNTIMED_CASE = 0.02;
|
|
55222
|
-
var SPLIT_PAST_SECONDS = 10;
|
|
55223
|
-
var ENGINE_START_SECONDS = 5;
|
|
55224
|
-
function median(values) {
|
|
55225
|
-
if (values.length === 0) {
|
|
55226
|
-
return;
|
|
55227
|
-
}
|
|
55228
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
55229
|
-
const middle = Math.floor(sorted.length / 2);
|
|
55230
|
-
return sorted.length % 2 === 1 ? sorted[middle] : ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2;
|
|
55231
|
-
}
|
|
55232
|
-
function unitsOf(suites, times, splitPast = SPLIT_PAST_SECONDS) {
|
|
55233
|
-
const known = Object.values(times);
|
|
55234
|
-
const caseCost = median(known.flatMap((time) => Object.values(time.cases))) ?? UNTIMED_CASE;
|
|
55235
|
-
const aroundCost = median(known.map((time) => time.seconds - Object.values(time.cases).reduce((sum, s) => sum + s, 0))) ?? 0;
|
|
55236
|
-
return suites.flatMap((suite) => {
|
|
55237
|
-
const time = times[suite.path];
|
|
55238
|
-
const costOf = (func) => time?.cases[func] ?? caseCost;
|
|
55239
|
-
const around = time === undefined ? aroundCost : Math.max(0, time.seconds - Object.values(time.cases).reduce((sum, s) => sum + s, 0));
|
|
55240
|
-
const whole = around + suite.cases.reduce((sum, func) => sum + costOf(func), 0);
|
|
55241
|
-
if (suite.windowed || whole <= splitPast || suite.cases.length < 2) {
|
|
55242
|
-
return [{ suite: suite.path, cases: suite.cases, cost: whole, windowed: suite.windowed }];
|
|
55243
|
-
}
|
|
55244
|
-
return suite.cases.map((func) => ({
|
|
55245
|
-
suite: suite.path,
|
|
55246
|
-
cases: [func],
|
|
55247
|
-
cost: around + costOf(func),
|
|
55248
|
-
windowed: false
|
|
55249
|
-
}));
|
|
55250
|
-
});
|
|
55251
|
-
}
|
|
55252
|
-
function dealt(units, declared, most) {
|
|
55253
|
-
const windowed = units.filter((unit) => unit.windowed);
|
|
55254
|
-
const headless = units.filter((unit) => !unit.windowed).sort((a, b) => b.cost - a.cost || a.suite.localeCompare(b.suite) || (a.cases[0] ?? "").localeCompare(b.cases[0] ?? ""));
|
|
55255
|
-
const total = headless.reduce((sum, unit) => sum + unit.cost, 0);
|
|
55256
|
-
const slowest = headless[0]?.cost ?? 0;
|
|
55257
|
-
const room = Math.max(1, most - (windowed.length > 0 && most > 1 ? 1 : 0));
|
|
55258
|
-
const count = headless.length === 0 ? 0 : Math.max(1, Math.min(room, headless.length, Math.ceil(total / Math.max(slowest, ENGINE_START_SECONDS))));
|
|
55259
|
-
const bins = Array.from({ length: count }, () => []);
|
|
55260
|
-
const loads = Array.from({ length: count }, () => 0);
|
|
55261
|
-
for (const unit of headless) {
|
|
55262
|
-
const lightest = loads.indexOf(Math.min(...loads));
|
|
55263
|
-
bins[lightest]?.push(unit);
|
|
55264
|
-
loads[lightest] = (loads[lightest] ?? 0) + unit.cost;
|
|
55265
|
-
}
|
|
55266
|
-
const plans = bins.map((bin) => planOf(bin, declared, false));
|
|
55267
|
-
return windowed.length > 0 ? [planOf(windowed, declared, true), ...plans] : plans;
|
|
55268
|
-
}
|
|
55269
|
-
function inOneEngine(units, declared) {
|
|
55270
|
-
return planOf(units, declared, units.some((unit) => unit.windowed));
|
|
55271
|
-
}
|
|
55272
|
-
function planOf(bin, declared, windowed) {
|
|
55273
|
-
const held = new Map;
|
|
55274
|
-
for (const unit of bin) {
|
|
55275
|
-
held.set(unit.suite, [...held.get(unit.suite) ?? [], ...unit.cases]);
|
|
55276
|
-
}
|
|
55277
|
-
const skipping = [...held].flatMap(([path, cases]) => (declared.get(path) ?? []).filter((func) => !cases.includes(func)).map((func) => `${path}:${func}`));
|
|
55278
|
-
return {
|
|
55279
|
-
suites: [...held].map(([path, cases]) => ({ path, cases: [...cases].sort() })),
|
|
55280
|
-
skipping,
|
|
55281
|
-
cost: bin.reduce((sum, unit) => sum + unit.cost, 0),
|
|
55282
|
-
windowed
|
|
55283
|
-
};
|
|
55284
|
-
}
|
|
55285
|
-
function viewportOf(projectPath) {
|
|
55286
|
-
const file = `${projectPath}/project.godot`;
|
|
55287
|
-
if (!existsSync17(file)) {
|
|
55288
|
-
return null;
|
|
55289
|
-
}
|
|
55290
|
-
const text = readFileSync15(file, "utf8");
|
|
55291
|
-
const width = /^window\/size\/viewport_width\s*=\s*(\d+)/m.exec(text)?.[1];
|
|
55292
|
-
const height = /^window\/size\/viewport_height\s*=\s*(\d+)/m.exec(text)?.[1];
|
|
55293
|
-
return width === undefined || height === undefined ? null : { width: Number(width), height: Number(height) };
|
|
55294
|
-
}
|
|
55492
|
+
// src/suite-run.ts
|
|
55493
|
+
init_suite_judge();
|
|
55494
|
+
init_suite_plan();
|
|
55295
55495
|
|
|
55296
55496
|
// src/version.ts
|
|
55297
|
-
import { readFileSync as
|
|
55298
|
-
import { dirname as dirname11, join as
|
|
55497
|
+
import { readFileSync as readFileSync18 } from "node:fs";
|
|
55498
|
+
import { dirname as dirname11, join as join21 } from "node:path";
|
|
55299
55499
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
55300
55500
|
function getLocalVersion() {
|
|
55301
55501
|
const currentDirectory = dirname11(fileURLToPath2(import.meta.url));
|
|
55302
55502
|
const packageCandidates = [
|
|
55303
|
-
|
|
55304
|
-
|
|
55503
|
+
join21(currentDirectory, "..", "package.json"),
|
|
55504
|
+
join21(currentDirectory, "..", "..", "package.json")
|
|
55305
55505
|
];
|
|
55306
55506
|
for (const packagePath of packageCandidates) {
|
|
55307
55507
|
try {
|
|
55308
|
-
const value = JSON.parse(
|
|
55508
|
+
const value = JSON.parse(readFileSync18(packagePath, "utf-8"));
|
|
55309
55509
|
if (typeof value === "object" && value !== null && "name" in value && value.name === "gdharness" && "version" in value && typeof value.version === "string") {
|
|
55310
55510
|
return value.version;
|
|
55311
55511
|
}
|
|
@@ -55357,9 +55557,9 @@ function testOptionsOf(args, cwd) {
|
|
|
55357
55557
|
}
|
|
55358
55558
|
const first = positional[0];
|
|
55359
55559
|
const named = first === undefined ? null : resolve4(cwd, first);
|
|
55360
|
-
const projectNamed = named !== null &&
|
|
55560
|
+
const projectNamed = named !== null && existsSync19(join22(named, "project.godot"));
|
|
55361
55561
|
const projectPath = projectNamed ? named : resolve4(cwd);
|
|
55362
|
-
if (!
|
|
55562
|
+
if (!existsSync19(join22(projectPath, "project.godot"))) {
|
|
55363
55563
|
throw new TestUsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
55364
55564
|
}
|
|
55365
55565
|
const last = (flag) => valued.get(flag)?.at(-1);
|
|
@@ -55402,7 +55602,8 @@ function resPathOf(entry, projectPath, cwd) {
|
|
|
55402
55602
|
const caseAt = /^(.*\.gd):(\w+)$/.exec(entry);
|
|
55403
55603
|
const file = caseAt?.[1] ?? entry;
|
|
55404
55604
|
const fromProject = resolve4(projectPath, file);
|
|
55405
|
-
const
|
|
55605
|
+
const fromHere = resolve4(cwd, file);
|
|
55606
|
+
const absolute = isAbsolute4(file) || !existsSync19(fromProject) && existsSync19(fromHere) ? fromHere : fromProject;
|
|
55406
55607
|
const inside = relative3(projectPath, absolute);
|
|
55407
55608
|
if (inside.startsWith("..") || isAbsolute4(inside)) {
|
|
55408
55609
|
throw new TestUsageError(`${entry} is not inside the project at ${projectPath}.`);
|
|
@@ -55414,7 +55615,7 @@ function homeOf(projectPath) {
|
|
|
55414
55615
|
return mainCheckoutOf(projectPath) ?? projectPath;
|
|
55415
55616
|
}
|
|
55416
55617
|
function selectedSuites(scripts, options) {
|
|
55417
|
-
const roots = options.roots.length > 0 ? options.roots :
|
|
55618
|
+
const roots = options.roots.length > 0 ? options.roots : TEST_DIRECTORIES.filter((root) => existsSync19(join22(options.projectPath, root))).map((root) => `res://${root}`);
|
|
55418
55619
|
if (options.selected.length === 0) {
|
|
55419
55620
|
const found = scripts.suitesUnder(roots);
|
|
55420
55621
|
if (found.length === 0) {
|
|
@@ -55426,10 +55627,10 @@ function selectedSuites(scripts, options) {
|
|
|
55426
55627
|
for (const entry of options.selected) {
|
|
55427
55628
|
const caseAt = /^(res:\/\/.*\.gd):(\w+)$/.exec(entry);
|
|
55428
55629
|
const path = caseAt?.[1] ?? entry;
|
|
55429
|
-
const directory =
|
|
55430
|
-
const suites =
|
|
55630
|
+
const directory = join22(options.projectPath, path.slice(6));
|
|
55631
|
+
const suites = existsSync19(directory) && statSync12(directory).isDirectory() ? scripts.suitesUnder([path.replace(/\/$/, "")]) : [scripts.suite(path)].filter((suite) => suite !== null);
|
|
55431
55632
|
if (suites.length === 0) {
|
|
55432
|
-
throw new TestUsageError(
|
|
55633
|
+
throw new TestUsageError(existsSync19(directory) ? `${path} holds no gdUnit4 suite: nothing there extends GdUnitTestSuite and declares a test_ function.` : `Nothing at ${path} in the project at ${options.projectPath}.`);
|
|
55433
55634
|
}
|
|
55434
55635
|
for (const suite of suites) {
|
|
55435
55636
|
const picked = chosen.get(suite.path) ?? { suite, cases: new Set };
|
|
@@ -55448,9 +55649,32 @@ function selectedSuites(scripts, options) {
|
|
|
55448
55649
|
cases: suite.cases.filter((c) => cases.has(c))
|
|
55449
55650
|
}));
|
|
55450
55651
|
}
|
|
55451
|
-
function roomFor(most) {
|
|
55452
|
-
const byMemory = Math.floor((
|
|
55453
|
-
|
|
55652
|
+
function roomFor(most, processors, available) {
|
|
55653
|
+
const byMemory = Math.floor((available - MEMORY_KEPT) / MEMORY_PER_ENGINE);
|
|
55654
|
+
const engines = Math.max(1, Math.min(most, processors, byMemory));
|
|
55655
|
+
if (engines >= most) {
|
|
55656
|
+
return { engines, held: undefined };
|
|
55657
|
+
}
|
|
55658
|
+
const gigabytes = (bytes) => `${(bytes / 1024 ** 3).toFixed(1)} GB`;
|
|
55659
|
+
return {
|
|
55660
|
+
engines,
|
|
55661
|
+
held: byMemory < processors ? `the machine has ${gigabytes(Math.max(0, available))} of memory free, ${gigabytes(MEMORY_KEPT)} of it kept for everything else and ${gigabytes(MEMORY_PER_ENGINE)} taken for each engine` : `the machine has ${plural(processors, "processor")}`
|
|
55662
|
+
};
|
|
55663
|
+
}
|
|
55664
|
+
async function memoryAvailable() {
|
|
55665
|
+
const level = await macMemoryLevel();
|
|
55666
|
+
return level === null ? freemem() : totalmem() * level / 100;
|
|
55667
|
+
}
|
|
55668
|
+
async function macMemoryLevel() {
|
|
55669
|
+
if (process12.platform !== "darwin") {
|
|
55670
|
+
return null;
|
|
55671
|
+
}
|
|
55672
|
+
try {
|
|
55673
|
+
const level = Number((await runHidden("sysctl", ["-n", "kern.memorystatus_level"])).stdout.trim());
|
|
55674
|
+
return Number.isFinite(level) && level > 0 && level <= 100 ? level : null;
|
|
55675
|
+
} catch {
|
|
55676
|
+
return null;
|
|
55677
|
+
}
|
|
55454
55678
|
}
|
|
55455
55679
|
function ordered(units, order) {
|
|
55456
55680
|
const sorted = [...units].sort((a, b) => a.suite.localeCompare(b.suite));
|
|
@@ -55483,17 +55707,17 @@ async function runTestCommand(options, godotPath) {
|
|
|
55483
55707
|
const startedAt = Date.now();
|
|
55484
55708
|
const scripts = new ProjectScripts(options.projectPath);
|
|
55485
55709
|
const home = homeOf(options.projectPath);
|
|
55486
|
-
const state =
|
|
55487
|
-
const timesFile =
|
|
55488
|
-
const passedFile =
|
|
55710
|
+
const state = join22(home, ".godot", "gdharness");
|
|
55711
|
+
const timesFile = join22(state, "suite-times.json");
|
|
55712
|
+
const passedFile = join22(state, "suite-passed.json");
|
|
55489
55713
|
let suites = selectedSuites(scripts, options);
|
|
55490
55714
|
const wholeSuites = suites.map((suite) => scripts.suite(suite.path) ?? suite);
|
|
55491
55715
|
const declared = new Map(wholeSuites.map((suite) => [suite.path, suite.cases]));
|
|
55492
|
-
const testRuns =
|
|
55716
|
+
const testRuns = join22(options.projectPath, ".godot", "gdharness", "test-runs");
|
|
55493
55717
|
sweepRunDirectories(testRuns);
|
|
55494
|
-
const runDirectory =
|
|
55718
|
+
const runDirectory = join22(testRuns, `${String(startedAt)}-${String(process12.pid)}`);
|
|
55495
55719
|
mkdirSync8(runDirectory, { recursive: true });
|
|
55496
|
-
const slots = new EngineSlots(options.slotsDirectory ??
|
|
55720
|
+
const slots = new EngineSlots(options.slotsDirectory ?? join22(home, ".godot", "gate", "slots"), options.slotCount ?? Math.min(MOST_ENGINES, availableParallelism()), { pid: process12.pid, started: await ownStart() });
|
|
55497
55721
|
const running = new Set;
|
|
55498
55722
|
const interrupted = () => {
|
|
55499
55723
|
for (const end of running) {
|
|
@@ -55522,9 +55746,9 @@ async function runTestCommand(options, godotPath) {
|
|
|
55522
55746
|
}
|
|
55523
55747
|
const fingerprints = new SuiteFingerprints(scripts, [
|
|
55524
55748
|
getLocalVersion(),
|
|
55525
|
-
|
|
55749
|
+
readFileSync19(shippedListedRunner(), "utf8"),
|
|
55526
55750
|
...options.reachAlso.map((path) => `${path}
|
|
55527
|
-
${
|
|
55751
|
+
${existsSync19(path) ? readFileSync19(path, "latin1") : "missing"}`)
|
|
55528
55752
|
]);
|
|
55529
55753
|
const printsOf = new Map(suites.map((suite) => [suite.path, fingerprints.of(suite)]));
|
|
55530
55754
|
if (options.changedOnly) {
|
|
@@ -55535,7 +55759,7 @@ ${existsSync18(path) ? readFileSync17(path, "latin1") : "missing"}`)
|
|
|
55535
55759
|
});
|
|
55536
55760
|
suites = suites.filter((suite) => !unchanged.includes(suite));
|
|
55537
55761
|
if (unchanged.length > 0) {
|
|
55538
|
-
console.log(`${plural(unchanged.length, "suite")} passed with everything ${unchanged.length === 1 ? "it reaches" : "they reach"} as it stands now, and ${unchanged.length === 1 ? "is" : "are"} not run.`);
|
|
55762
|
+
console.log(`${plural(unchanged.length, "suite")} passed with everything ${unchanged.length === 1 ? "it reaches" : "they reach"} as it stands now, and ${unchanged.length === 1 ? "is" : "are"} not run: ${named(unchanged.map((suite) => suite.path))}.`);
|
|
55539
55763
|
}
|
|
55540
55764
|
if (suites.length === 0) {
|
|
55541
55765
|
console.log("Nothing has changed since every suite last passed.");
|
|
@@ -55545,9 +55769,16 @@ ${existsSync18(path) ? readFileSync17(path, "latin1") : "missing"}`)
|
|
|
55545
55769
|
}
|
|
55546
55770
|
const times = readTimes(timesFile);
|
|
55547
55771
|
const units = unitsOf(suites, times);
|
|
55548
|
-
const
|
|
55772
|
+
const room = roomFor(options.engines, availableParallelism(), await memoryAvailable());
|
|
55773
|
+
const plans = options.order === null ? dealt(units, declared, room.engines) : [inOneEngine(ordered(units, options.order), declared)];
|
|
55549
55774
|
const functions = suites.reduce((sum, suite) => sum + suite.cases.length, 0);
|
|
55550
|
-
|
|
55775
|
+
const held = options.order === null && room.held !== undefined && plans.length === room.engines ? `, of the ${String(options.engines)} asked for, since ${room.held}` : "";
|
|
55776
|
+
const how = options.order === null ? `${windowedSaid(plans)}${held}` : options.order === "reversed" ? ", in reverse order of path" : `, shuffled by ${String(options.order)}`;
|
|
55777
|
+
console.log(`${plural(functions, "test function")} in ${plural(suites.length, "suite")} over ${plural(plans.length, "engine")}${how}.`);
|
|
55778
|
+
const split = [...new Set(units.map((unit) => unit.suite))].filter((path) => units.filter((unit) => unit.suite === path).length > 1);
|
|
55779
|
+
if (plans.length > 1 && split.length > 0) {
|
|
55780
|
+
console.log(`Dealt out case by case, since ${split.length === 1 ? "it" : "each"} took more than ${String(SPLIT_PAST_SECONDS)} s last time: ${named(split)}.`);
|
|
55781
|
+
}
|
|
55551
55782
|
let ran = await Promise.all(plans.map((plan, index) => runEngine2(launch, plan, index, units)));
|
|
55552
55783
|
if (ran.some((engine) => engine.verdict.starved) && ran.some((engine) => !engine.verdict.passed)) {
|
|
55553
55784
|
console.log("An engine ran out of memory, so every engine that failed runs again, one at a time.");
|
|
@@ -55570,7 +55801,7 @@ function plural(count, noun) {
|
|
|
55570
55801
|
}
|
|
55571
55802
|
function readRecord(file) {
|
|
55572
55803
|
try {
|
|
55573
|
-
const parsed = JSON.parse(
|
|
55804
|
+
const parsed = JSON.parse(readFileSync19(file, "utf8"));
|
|
55574
55805
|
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
55575
55806
|
} catch {
|
|
55576
55807
|
return {};
|
|
@@ -55597,20 +55828,20 @@ function recordPassed(file, ran, printsOf) {
|
|
|
55597
55828
|
writeJson(file, passed);
|
|
55598
55829
|
}
|
|
55599
55830
|
function sweepRunDirectories(testRuns) {
|
|
55600
|
-
if (!
|
|
55831
|
+
if (!existsSync19(testRuns)) {
|
|
55601
55832
|
return;
|
|
55602
55833
|
}
|
|
55603
|
-
for (const entry of
|
|
55834
|
+
for (const entry of readdirSync12(testRuns)) {
|
|
55604
55835
|
const pid = Number(/^\d+-(\d+)$/.exec(entry)?.[1]);
|
|
55605
55836
|
if (!Number.isInteger(pid) || !alive(pid)) {
|
|
55606
|
-
rmSync10(
|
|
55837
|
+
rmSync10(join22(testRuns, entry), { recursive: true, force: true });
|
|
55607
55838
|
}
|
|
55608
55839
|
}
|
|
55609
55840
|
}
|
|
55610
55841
|
async function importProject(launch) {
|
|
55611
|
-
const directory =
|
|
55612
|
-
mkdirSync8(
|
|
55613
|
-
const log =
|
|
55842
|
+
const directory = join22(launch.runDirectory, "import");
|
|
55843
|
+
mkdirSync8(join22(directory, "user"), { recursive: true });
|
|
55844
|
+
const log = join22(directory, "import.log");
|
|
55614
55845
|
const lock = await launch.slots.take();
|
|
55615
55846
|
try {
|
|
55616
55847
|
const ended = await watched(launch, ["--headless", "--audio-driver", "Dummy", "--path", launch.projectPath, "--import"], directory, log, false, launch.silenceFloorSeconds);
|
|
@@ -55623,11 +55854,11 @@ async function importProject(launch) {
|
|
|
55623
55854
|
}
|
|
55624
55855
|
}
|
|
55625
55856
|
async function runEngine2(launch, plan, index, units, again = "") {
|
|
55626
|
-
const directory =
|
|
55627
|
-
mkdirSync8(
|
|
55628
|
-
const suitesFile =
|
|
55629
|
-
const skippingFile =
|
|
55630
|
-
const expectedFile =
|
|
55857
|
+
const directory = join22(launch.runDirectory, `engine-${String(index)}${again === "" ? "" : `-${again}`}`);
|
|
55858
|
+
mkdirSync8(join22(directory, "user"), { recursive: true });
|
|
55859
|
+
const suitesFile = join22(directory, "suites.txt");
|
|
55860
|
+
const skippingFile = join22(directory, "skipping.txt");
|
|
55861
|
+
const expectedFile = join22(directory, "expected.json");
|
|
55631
55862
|
writeFileSync10(suitesFile, `${plan.suites.map((suite) => suite.path).join(`
|
|
55632
55863
|
`)}
|
|
55633
55864
|
`);
|
|
@@ -55647,7 +55878,7 @@ async function runEngine2(launch, plan, index, units, again = "") {
|
|
|
55647
55878
|
...plan.windowed ? [] : ["--ignoreHeadlessMode"],
|
|
55648
55879
|
"-c",
|
|
55649
55880
|
"-rd",
|
|
55650
|
-
`res://${relative3(launch.projectPath,
|
|
55881
|
+
`res://${relative3(launch.projectPath, join22(directory, "reports")).split(sep2).join("/")}`,
|
|
55651
55882
|
"-rc",
|
|
55652
55883
|
"1",
|
|
55653
55884
|
"--",
|
|
@@ -55664,7 +55895,7 @@ async function runEngine2(launch, plan, index, units, again = "") {
|
|
|
55664
55895
|
const held = new Set(plan.suites.map((suite) => suite.path));
|
|
55665
55896
|
const slowest = Math.max(0, ...units.filter((unit) => held.has(unit.suite)).map((unit) => unit.cost));
|
|
55666
55897
|
const silence = Math.max(launch.silenceFloorSeconds, SILENCE_TIMES_SLOWEST * slowest);
|
|
55667
|
-
const log =
|
|
55898
|
+
const log = join22(directory, "engine.log");
|
|
55668
55899
|
const lock = await launch.slots.take();
|
|
55669
55900
|
const began = Date.now();
|
|
55670
55901
|
let end;
|
|
@@ -55673,7 +55904,7 @@ async function runEngine2(launch, plan, index, units, again = "") {
|
|
|
55673
55904
|
} finally {
|
|
55674
55905
|
launch.slots.release(lock);
|
|
55675
55906
|
}
|
|
55676
|
-
const printed = readRun(
|
|
55907
|
+
const printed = readRun(readFileSync19(log, "utf8"));
|
|
55677
55908
|
const assignment = { suites: plan.suites, expected: readExpected(expectedFile) };
|
|
55678
55909
|
return {
|
|
55679
55910
|
index,
|
|
@@ -55684,16 +55915,8 @@ async function runEngine2(launch, plan, index, units, again = "") {
|
|
|
55684
55915
|
seconds: (Date.now() - began) / 1000
|
|
55685
55916
|
};
|
|
55686
55917
|
}
|
|
55687
|
-
function readExpected(file) {
|
|
55688
|
-
try {
|
|
55689
|
-
const parsed = JSON.parse(readFileSync17(file, "utf8"));
|
|
55690
|
-
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
55691
|
-
} catch {
|
|
55692
|
-
return {};
|
|
55693
|
-
}
|
|
55694
|
-
}
|
|
55695
55918
|
async function watched(launch, args, directory, log, windowed, silenceSeconds) {
|
|
55696
|
-
const user =
|
|
55919
|
+
const user = join22(directory, "user");
|
|
55697
55920
|
const env = { ...process12.env, APPDATA: user, XDG_DATA_HOME: user };
|
|
55698
55921
|
const output = openSync3(log, "w");
|
|
55699
55922
|
let child;
|
|
@@ -55705,12 +55928,12 @@ async function watched(launch, args, directory, log, windowed, silenceSeconds) {
|
|
|
55705
55928
|
const how = windowed ? "on a desktop of its own" : "through the desktop helper";
|
|
55706
55929
|
return { code: null, signal: null, ended: `the engine could not be started ${how}: ${compiled}` };
|
|
55707
55930
|
}
|
|
55708
|
-
const stopFile =
|
|
55931
|
+
const stopFile = join22(directory, "stop");
|
|
55709
55932
|
const how = windowed ? { desktop: HIDDEN_DESKTOP, directory: launch.projectPath } : { directory: launch.projectPath };
|
|
55710
55933
|
const wrapper = throughHelper(how, launch.godotPath, args, {
|
|
55711
|
-
spec:
|
|
55712
|
-
pid:
|
|
55713
|
-
error:
|
|
55934
|
+
spec: join22(directory, "spec.json"),
|
|
55935
|
+
pid: join22(directory, "pid"),
|
|
55936
|
+
error: join22(directory, "error"),
|
|
55714
55937
|
stop: stopFile
|
|
55715
55938
|
});
|
|
55716
55939
|
child = spawnHidden(wrapper.command, wrapper.args, {
|
|
@@ -55813,12 +56036,17 @@ function report(ran, startedAt, runDirectory) {
|
|
|
55813
56036
|
const suites = new Set(ran.flatMap((engine) => engine.plan.suites.map((suite) => suite.path))).size;
|
|
55814
56037
|
const longest = Math.max(0, ...ran.map((engine) => engine.seconds));
|
|
55815
56038
|
const took = (Date.now() - startedAt) / 1000;
|
|
56039
|
+
const totals = `${plural(cases, "case")} in ${plural(suites, "suite")} across ${plural(ran.length, "engine")}, ${took.toFixed(1)} s; the longest engine took ${longest.toFixed(1)} s.`;
|
|
56040
|
+
console.log("");
|
|
55816
56041
|
if (failed.length === 0) {
|
|
55817
56042
|
rmSync10(runDirectory, { recursive: true, force: true });
|
|
56043
|
+
console.log(`Passed: ${totals}`);
|
|
56044
|
+
return 0;
|
|
55818
56045
|
}
|
|
55819
|
-
|
|
55820
|
-
console.log(
|
|
55821
|
-
|
|
56046
|
+
const problems = failed.reduce((sum, engine) => sum + engine.verdict.faults.length, 0);
|
|
56047
|
+
console.log(`Failed: ${plural(problems, "problem")}, listed above. Ran ${totals}`);
|
|
56048
|
+
console.log(`Every engine's output is kept under ${runDirectory} until the next run.`);
|
|
56049
|
+
return 1;
|
|
55822
56050
|
}
|
|
55823
56051
|
|
|
55824
56052
|
// src/cli.ts
|