gdharness 1.1.45 → 1.1.47
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 +217 -51
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +97 -3
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +13 -2
- package/build/godot/addons/gdharness_runtime/runtime_menus.gd +1 -1
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +160 -41
- package/build/godot/addons/gdharness_runtime/runtime_says.gd +26 -2
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +183 -12
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +14 -3
- package/build/index.js +218 -58
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -10259,7 +10259,7 @@ import {
|
|
|
10259
10259
|
rmdirSync as rmdirSync2,
|
|
10260
10260
|
statSync as statSync8
|
|
10261
10261
|
} from "node:fs";
|
|
10262
|
-
import { basename as basename5, dirname as dirname14, extname as extname2, isAbsolute as isAbsolute3, join as
|
|
10262
|
+
import { basename as basename5, dirname as dirname14, extname as extname2, isAbsolute as isAbsolute3, join as join21, normalize as normalize2, relative as relative4 } from "node:path";
|
|
10263
10263
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10264
10264
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
10265
10265
|
import { promisify as promisify5 } from "node:util";
|
|
@@ -29420,12 +29420,16 @@ function whyNoReport(printed, asked) {
|
|
|
29420
29420
|
}
|
|
29421
29421
|
var DISCOVERY_FAILED = "Script errors were detected during test discovery!";
|
|
29422
29422
|
function scriptErrorsPrinted(printed) {
|
|
29423
|
+
return discoveryListing(printed)?.errors ?? [];
|
|
29424
|
+
}
|
|
29425
|
+
function discoveryListing(printed) {
|
|
29423
29426
|
const start = printed.findIndex((line) => line.includes(DISCOVERY_FAILED));
|
|
29424
29427
|
if (start < 0) {
|
|
29425
|
-
return
|
|
29428
|
+
return null;
|
|
29426
29429
|
}
|
|
29427
29430
|
const found = [];
|
|
29428
29431
|
let message = null;
|
|
29432
|
+
let to = start + 1;
|
|
29429
29433
|
for (const line of printed.slice(start + 1)) {
|
|
29430
29434
|
const at = /^\s*at (res:\/\/.+):(\d+)\s*$/.exec(line);
|
|
29431
29435
|
if (at !== null && message !== null) {
|
|
@@ -29436,8 +29440,10 @@ function scriptErrorsPrinted(printed) {
|
|
|
29436
29440
|
} else {
|
|
29437
29441
|
break;
|
|
29438
29442
|
}
|
|
29443
|
+
to += 1;
|
|
29439
29444
|
}
|
|
29440
|
-
|
|
29445
|
+
const errors = found.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : (a.line ?? 0) - (b.line ?? 0));
|
|
29446
|
+
return { errors, from: start, to };
|
|
29441
29447
|
}
|
|
29442
29448
|
var ESCAPE = String.fromCharCode(27);
|
|
29443
29449
|
var LOCATION = `'[^'\\n]*' in \\S+:\\d+`;
|
|
@@ -32498,6 +32504,99 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
32498
32504
|
}
|
|
32499
32505
|
}
|
|
32500
32506
|
|
|
32507
|
+
// src/script-origins.ts
|
|
32508
|
+
import { join as join19 } from "node:path";
|
|
32509
|
+
var UNRESOLVED = /Could not resolve class "([A-Za-z_][A-Za-z0-9_]*)", because of a parser error/;
|
|
32510
|
+
function unresolvedClassIn(message) {
|
|
32511
|
+
return UNRESOLVED.exec(message)?.[1] ?? null;
|
|
32512
|
+
}
|
|
32513
|
+
var MOST_CHECKED = 8;
|
|
32514
|
+
async function originsOf(errors, declared, check) {
|
|
32515
|
+
const listed = new Set(errors.map((error) => error.path));
|
|
32516
|
+
const origins = [];
|
|
32517
|
+
const unconfirmed = [];
|
|
32518
|
+
const asked = new Set;
|
|
32519
|
+
const waiting = errors.map((error) => unresolvedClassIn(error.message)).filter((name) => name !== null);
|
|
32520
|
+
while (waiting.length > 0 && asked.size < MOST_CHECKED) {
|
|
32521
|
+
const name = waiting.shift() ?? "";
|
|
32522
|
+
const path = declared.get(name);
|
|
32523
|
+
if (path === undefined || listed.has(path) || asked.has(path)) {
|
|
32524
|
+
continue;
|
|
32525
|
+
}
|
|
32526
|
+
asked.add(path);
|
|
32527
|
+
const checked = await check(path);
|
|
32528
|
+
const own = checked.errors.filter((error) => error.path === path);
|
|
32529
|
+
if (own.length === 0) {
|
|
32530
|
+
unconfirmed.push({
|
|
32531
|
+
path,
|
|
32532
|
+
declares: name,
|
|
32533
|
+
why: checked.failure === null ? "the engine's check of it found no error" : `the engine's check of it did not finish (${checked.failure})`
|
|
32534
|
+
});
|
|
32535
|
+
continue;
|
|
32536
|
+
}
|
|
32537
|
+
for (const error of own) {
|
|
32538
|
+
origins.push({ ...error, declares: name });
|
|
32539
|
+
const deeper = unresolvedClassIn(error.message);
|
|
32540
|
+
if (deeper !== null) {
|
|
32541
|
+
waiting.push(deeper);
|
|
32542
|
+
}
|
|
32543
|
+
}
|
|
32544
|
+
}
|
|
32545
|
+
return { origins, unconfirmed };
|
|
32546
|
+
}
|
|
32547
|
+
var RELOADED_AT = /GDScript::reload \((res:\/\/.+):(\d+)\)$/;
|
|
32548
|
+
function reloadedAt(entry) {
|
|
32549
|
+
for (const line of entry.detail) {
|
|
32550
|
+
const at = RELOADED_AT.exec(line.trim());
|
|
32551
|
+
if (at !== null) {
|
|
32552
|
+
return { path: at[1] ?? "", line: Number(at[2]) === 0 ? null : Number(at[2]) };
|
|
32553
|
+
}
|
|
32554
|
+
}
|
|
32555
|
+
return null;
|
|
32556
|
+
}
|
|
32557
|
+
function checkWithEngine(godotPath, projectPath) {
|
|
32558
|
+
return async (path) => {
|
|
32559
|
+
const logDir = scratchDirectory("tests");
|
|
32560
|
+
try {
|
|
32561
|
+
const ran = await runEngine(godotPath, [
|
|
32562
|
+
"--headless",
|
|
32563
|
+
"--log-file",
|
|
32564
|
+
join19(logDir, "engine.log"),
|
|
32565
|
+
"--path",
|
|
32566
|
+
projectPath,
|
|
32567
|
+
"--check-only",
|
|
32568
|
+
"-s",
|
|
32569
|
+
path
|
|
32570
|
+
], { timeout: { ms: 60000, said: "minute" } });
|
|
32571
|
+
const errors = ran.log.select({ severity: "error", sinceLastCall: false, limit: 50 }).entries.flatMap((entry) => {
|
|
32572
|
+
const at = reloadedAt(entry);
|
|
32573
|
+
return at === null ? [] : [{ ...at, message: entry.text }];
|
|
32574
|
+
});
|
|
32575
|
+
return { errors, failure: ran.failure === null ? null : howItEnded(ran) };
|
|
32576
|
+
} finally {
|
|
32577
|
+
discard(logDir);
|
|
32578
|
+
}
|
|
32579
|
+
};
|
|
32580
|
+
}
|
|
32581
|
+
function withoutListed(entries, errors) {
|
|
32582
|
+
if (errors.length === 0) {
|
|
32583
|
+
return [...entries];
|
|
32584
|
+
}
|
|
32585
|
+
const listing = discoveryListing(entries.map((entry) => entry.text));
|
|
32586
|
+
const paths = new Set(errors.map((error) => error.path));
|
|
32587
|
+
return entries.filter((entry, at) => {
|
|
32588
|
+
if (listing !== null && at >= listing.from && at < listing.to) {
|
|
32589
|
+
return false;
|
|
32590
|
+
}
|
|
32591
|
+
const reloaded = reloadedAt(entry);
|
|
32592
|
+
if (reloaded !== null && errors.some((error) => error.path === reloaded.path && error.line === reloaded.line && error.message === entry.text)) {
|
|
32593
|
+
return false;
|
|
32594
|
+
}
|
|
32595
|
+
const failedToLoad = /^Failed to load script "(res:\/\/[^"]+)" with error "[^"]+"\.$/.exec(entry.text);
|
|
32596
|
+
return failedToLoad === null || !paths.has(failedToLoad[1] ?? "");
|
|
32597
|
+
});
|
|
32598
|
+
}
|
|
32599
|
+
|
|
32501
32600
|
// src/setup.ts
|
|
32502
32601
|
import { spawnSync } from "node:child_process";
|
|
32503
32602
|
import { createHash as createHash4 } from "node:crypto";
|
|
@@ -32512,7 +32611,7 @@ import {
|
|
|
32512
32611
|
statSync as statSync7,
|
|
32513
32612
|
writeFileSync as writeFileSync13
|
|
32514
32613
|
} from "node:fs";
|
|
32515
|
-
import { dirname as dirname13, join as
|
|
32614
|
+
import { dirname as dirname13, join as join20 } from "node:path";
|
|
32516
32615
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
32517
32616
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
32518
32617
|
var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
|
|
@@ -32536,7 +32635,7 @@ function addonsDigest(names, from) {
|
|
|
32536
32635
|
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
32537
32636
|
continue;
|
|
32538
32637
|
}
|
|
32539
|
-
const path =
|
|
32638
|
+
const path = join20(directory, name);
|
|
32540
32639
|
const inside = `${relative}/${name}`;
|
|
32541
32640
|
if (statSync7(path).isDirectory()) {
|
|
32542
32641
|
walk(path, inside);
|
|
@@ -32550,7 +32649,7 @@ function addonsDigest(names, from) {
|
|
|
32550
32649
|
}
|
|
32551
32650
|
};
|
|
32552
32651
|
for (const name of names) {
|
|
32553
|
-
walk(
|
|
32652
|
+
walk(join20(from, name), name);
|
|
32554
32653
|
}
|
|
32555
32654
|
return hash.digest("hex");
|
|
32556
32655
|
}
|
|
@@ -32583,7 +32682,7 @@ function installedRuntimeDigest(projectPath) {
|
|
|
32583
32682
|
return digestMarker(projectPath, RUNTIME_ADDON);
|
|
32584
32683
|
}
|
|
32585
32684
|
function digestMarker(projectPath, addon) {
|
|
32586
|
-
const marker =
|
|
32685
|
+
const marker = join20(projectPath, "addons", addon, DIGEST_MARKER);
|
|
32587
32686
|
if (!existsSync15(marker)) {
|
|
32588
32687
|
return null;
|
|
32589
32688
|
}
|
|
@@ -32591,10 +32690,10 @@ function digestMarker(projectPath, addon) {
|
|
|
32591
32690
|
return said === "" ? null : said;
|
|
32592
32691
|
}
|
|
32593
32692
|
function shippedAddonsDirectory() {
|
|
32594
|
-
return
|
|
32693
|
+
return join20(dirname13(fileURLToPath3(import.meta.url)), "godot", "addons");
|
|
32595
32694
|
}
|
|
32596
32695
|
function installedAddonVersion(projectPath) {
|
|
32597
|
-
const marker =
|
|
32696
|
+
const marker = join20(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
32598
32697
|
if (!existsSync15(marker)) {
|
|
32599
32698
|
return null;
|
|
32600
32699
|
}
|
|
@@ -32609,7 +32708,7 @@ function loaderAmong(projectPath, named) {
|
|
|
32609
32708
|
}
|
|
32610
32709
|
let source = null;
|
|
32611
32710
|
try {
|
|
32612
|
-
source = readFileSync18(
|
|
32711
|
+
source = readFileSync18(join20(projectPath, path), "utf8");
|
|
32613
32712
|
} catch {}
|
|
32614
32713
|
if (source?.includes(RUNTIME_AUTOLOAD.path)) {
|
|
32615
32714
|
return { name: entry, path, exists: true };
|
|
@@ -32651,8 +32750,8 @@ function enabledPlugins(settings) {
|
|
|
32651
32750
|
function inspectProject(projectPath) {
|
|
32652
32751
|
const problems = [];
|
|
32653
32752
|
const addons = ADDONS.map((name) => {
|
|
32654
|
-
const directory =
|
|
32655
|
-
const marker =
|
|
32753
|
+
const directory = join20(projectPath, "addons", name);
|
|
32754
|
+
const marker = join20(directory, VERSION_MARKER);
|
|
32656
32755
|
const installed = existsSync15(directory);
|
|
32657
32756
|
const version = existsSync15(marker) ? readFileSync18(marker, "utf8").trim() : null;
|
|
32658
32757
|
const current = version === SERVER_VERSION;
|
|
@@ -32663,7 +32762,7 @@ function inspectProject(projectPath) {
|
|
|
32663
32762
|
}
|
|
32664
32763
|
return { name, installed, version, current };
|
|
32665
32764
|
});
|
|
32666
|
-
const settings = parseProjectGodot(readFileSync18(
|
|
32765
|
+
const settings = parseProjectGodot(readFileSync18(join20(projectPath, "project.godot"), "utf8"));
|
|
32667
32766
|
const pluginsEnabled = enabledPlugins(settings);
|
|
32668
32767
|
for (const name of EDITOR_PLUGINS) {
|
|
32669
32768
|
if (!pluginsEnabled.includes(name)) {
|
|
@@ -32679,7 +32778,7 @@ function inspectProject(projectPath) {
|
|
|
32679
32778
|
const tracked = trackedByGit(projectPath, [...named.values()]);
|
|
32680
32779
|
const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
|
|
32681
32780
|
for (const [name, path] of named) {
|
|
32682
|
-
if (!existsSync15(
|
|
32781
|
+
if (!existsSync15(join20(projectPath, path))) {
|
|
32683
32782
|
if (!addonMissing(path)) {
|
|
32684
32783
|
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`);
|
|
32685
32784
|
}
|
|
@@ -33085,13 +33184,13 @@ var TOOL_SPECS = [
|
|
|
33085
33184
|
},
|
|
33086
33185
|
{
|
|
33087
33186
|
name: "project_test",
|
|
33088
|
-
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. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. 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. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. 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. Needs gdUnit4 under addons/gdUnit4.",
|
|
33187
|
+
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. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. 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. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so 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. 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. Needs gdUnit4 under addons/gdUnit4.",
|
|
33089
33188
|
parameters: {
|
|
33090
33189
|
projectPath: PROJECT_PATH,
|
|
33091
33190
|
path: {
|
|
33092
33191
|
type: ["string", "array"],
|
|
33093
33192
|
items: { type: "string" },
|
|
33094
|
-
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.
|
|
33193
|
+
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."
|
|
33095
33194
|
},
|
|
33096
33195
|
ignore: {
|
|
33097
33196
|
type: "array",
|
|
@@ -33405,7 +33504,7 @@ var TOOL_SPECS = [
|
|
|
33405
33504
|
},
|
|
33406
33505
|
{
|
|
33407
33506
|
name: "editor_run",
|
|
33408
|
-
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. start and check both wait first for a scan the open editor is running and for the class cache it writes just after, since a game booted in between resolves no global class, as does every other engine this server starts on the project; waitedForEditorScanMs says how long, and scanNote says when a scan ran past thirty seconds and the game started during it. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for. A start this server made also answers with transcript, the file both the run's streams are written to, so a watch on it can be armed off the start rather than off a second call.",
|
|
33507
|
+
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. start and check both wait first for a scan the open editor is running and for the class cache it writes just after, since a game booted in between resolves no global class, as does every other engine this server starts on the project; waitedForEditorScanMs says how long, and scanNote says when a scan ran past thirty seconds and the game started during it. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for, and the note then says what a runtime_* call naming no projectPath meets until it announces. A start this server made also answers with transcript, the file both the run's streams are written to, so a watch on it can be armed off the start rather than off a second call.",
|
|
33409
33508
|
parameters: {
|
|
33410
33509
|
projectPath: {
|
|
33411
33510
|
...PROJECT_PATH,
|
|
@@ -33621,7 +33720,7 @@ var TOOL_SPECS = [
|
|
|
33621
33720
|
property: {
|
|
33622
33721
|
type: "string",
|
|
33623
33722
|
ops: ["property", "find"],
|
|
33624
|
-
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that
|
|
33723
|
+
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that only reads (an engine method the engine marks as reading, or a game method not declared -> void, so queue_free() is refused) and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. An engine method that reads takes literal arguments in its brackets, words in double quotes, numbers, true, false or null, so a control\'s theme is read where it is drawn: \'get_theme_stylebox("panel"):content_margin_top\' or \'get_theme_constant("separation")\'. A game method is given none in a path, since nothing marks one as only reading; runtime_invoke call takes those. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
|
|
33625
33724
|
},
|
|
33626
33725
|
depth: {
|
|
33627
33726
|
type: "integer",
|
|
@@ -33655,7 +33754,7 @@ var TOOL_SPECS = [
|
|
|
33655
33754
|
says: {
|
|
33656
33755
|
type: "string",
|
|
33657
33756
|
ops: ["find"],
|
|
33658
|
-
description: 'find: part of what the node has written on it as drawn, the way the text op reads it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern, and a glob that finds nothing says under note how many nodes it would find open at both ends. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Alternatives separated by | match when any one of them does, each by these rules ("*won*|*lost*"); write \\| for a bar that is part of the words.'
|
|
33757
|
+
description: 'find: part of what the node has written on it as drawn, the way the text op reads it, case-insensitively and with a typed space matching any space the game draws (a non-breaking one included), which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern, and a glob that finds nothing says under note how many nodes it would find open at both ends. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Alternatives separated by | match when any one of them does, each by these rules ("*won*|*lost*"); write \\| for a bar that is part of the words.'
|
|
33659
33758
|
},
|
|
33660
33759
|
limit: {
|
|
33661
33760
|
type: "integer",
|
|
@@ -33718,7 +33817,7 @@ var TOOL_SPECS = [
|
|
|
33718
33817
|
property: {
|
|
33719
33818
|
type: "string",
|
|
33720
33819
|
ops: ["set"],
|
|
33721
|
-
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method
|
|
33820
|
+
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a reading method returned, as runtime_inspect property describes. A map keyed by numbers is stepped into by the number, "_game:units:3". The answer reads back off the same holder, and a write that leaves the property as it was is refused and says what it still holds: the engine not taking it and a setter keeping the value look the same, so the refusal names neither. A write of the value the property already holds, at its own precision, as a float given in 64 bits to a property keeping 32, is not refused: it answers unchanged: true.'
|
|
33722
33821
|
},
|
|
33723
33822
|
value: {
|
|
33724
33823
|
ops: ["set"],
|
|
@@ -33728,12 +33827,18 @@ var TOOL_SPECS = [
|
|
|
33728
33827
|
method: {
|
|
33729
33828
|
type: "string",
|
|
33730
33829
|
ops: ["call"],
|
|
33731
|
-
description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, a list or a map is stepped into by index or key, "_game:run:roster:0:retire", and a step written as a call walks through what a method
|
|
33830
|
+
description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, a list or a map is stepped into by index or key, "_game:run:roster:0:retire", and a step written as a call walks through what a reading method returned, "get_viewport():gui_get_focus_owner", as runtime_inspect property describes.'
|
|
33732
33831
|
},
|
|
33733
33832
|
args: {
|
|
33734
33833
|
type: "array",
|
|
33735
33834
|
ops: ["call"],
|
|
33736
|
-
description: `call: the arguments, fitted to the method's parameter types. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths.`
|
|
33835
|
+
description: `call: the arguments, fitted to the method's parameter types. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
|
|
33836
|
+
},
|
|
33837
|
+
properties: {
|
|
33838
|
+
type: ["boolean", "array"],
|
|
33839
|
+
items: { type: "string" },
|
|
33840
|
+
ops: ["call"],
|
|
33841
|
+
description: 'call: read the object the call answered with in the same answer, under result_properties: true for every variable its script declares, or a list of names, each a colon path walked from that object the way property paths are ("dead_allowed", "terms:0:name"). A name that is not there is listed under result_properties_missing with why, and a result that is not an object answers properties_note instead. Without it an object comes back as its class and id only.'
|
|
33737
33842
|
},
|
|
33738
33843
|
timeoutMs: {
|
|
33739
33844
|
type: "integer",
|
|
@@ -33815,7 +33920,7 @@ var TOOL_SPECS = [
|
|
|
33815
33920
|
},
|
|
33816
33921
|
{
|
|
33817
33922
|
name: "runtime_input",
|
|
33818
|
-
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 or mouse motion. All of it works headless, where the window is 64 by 64 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:
|
|
33923
|
+
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 or mouse motion. All of it works headless, where the window is 64 by 64 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.",
|
|
33819
33924
|
parameters: {
|
|
33820
33925
|
projectPath: RUNNING_PROJECT_PATH,
|
|
33821
33926
|
pid: RUNNING_PID,
|
|
@@ -33827,7 +33932,7 @@ var TOOL_SPECS = [
|
|
|
33827
33932
|
says: {
|
|
33828
33933
|
type: "string",
|
|
33829
33934
|
ops: ["click"],
|
|
33830
|
-
description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it, and | separates alternatives), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it or a dialog stacked above its own, is not on screen; nor is one on a hidden canvas layer, one outside the viewport, or one a container clips away that a click does not scroll. The refusal counts those apart. The matches are ranked the way a person picks what to press: an enabled button before anything else, a disabled one included, then a control whose whole text, or one whole line of it such as the title on a card, is the words before one saying them as part of more, and the best one is pressed; found.picked says why. Several matches at the best rank are refused with each one listed, and index picks one."
|
|
33935
|
+
description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it, and | separates alternatives), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it or a dialog stacked above its own, is not on screen; nor is one on a hidden canvas layer, one outside the viewport, or one a container clips away that a click does not scroll. The refusal counts those apart. The matches are ranked the way a person picks what to press: an enabled button before anything else, a disabled one included, then a control whose whole text, or one whole line of it such as the title on a card, is the words before one saying them as part of more, and the best one is pressed; found.picked says why. Several matches at the best rank are refused with each one listed, and index picks one. A match saying the words only as part of more is named under found.partOf, and is refused when another control says exactly the words (hidden, off the screen, covered, or on screen taking clicks without being a button), since it would stand in for that one, and when it lets the pointer through to what is under it; index presses it anyway. Words found only inside longer words (\"out\" in \"Scout\") rank after every other match and are refused unless index is given; words starting a longer word (\"Recruit\" in \"Recruits\") count. An empty field says its placeholder, and a control with no text of its own, a picture on a button, says its accessibility name. An item of an open menu is drawn by the menu rather than held as a control, so a click finds none, and the refusal names the menu and the choose call that picks it."
|
|
33831
33936
|
},
|
|
33832
33937
|
action: {
|
|
33833
33938
|
type: "string",
|
|
@@ -34108,7 +34213,7 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
34108
34213
|
if (!existsSync16(directory)) {
|
|
34109
34214
|
return `outputPath's directory ${dirname14(outputPath)} does not exist; a capture creates no directories.`;
|
|
34110
34215
|
}
|
|
34111
|
-
const where =
|
|
34216
|
+
const where = join21(directory, basename5(outputPath));
|
|
34112
34217
|
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
34113
34218
|
if (inside !== undefined) {
|
|
34114
34219
|
return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
|
|
@@ -34411,10 +34516,13 @@ function runtimeVerdict(endpoint, after) {
|
|
|
34411
34516
|
return {
|
|
34412
34517
|
listening: false,
|
|
34413
34518
|
mayYetAnnounce: after.running,
|
|
34414
|
-
note: after.running ? `nothing announced itself within ${after.budgetMs}ms${sized} and ${state}, so it may announce a moment from now: editor_status says whether it has, and editor_run start takes runtimeWaitMs to wait longer than this${after.withArgs ? ", which a run carrying its own arguments may well need, since whatever they ask the game to do before its first frame is inside this wait" : ""}` : `nothing announced itself within ${after.budgetMs}ms${sized} and the game is no longer running, so nothing is going to: editor_output has what it printed on the way down`,
|
|
34519
|
+
note: after.running ? `nothing announced itself within ${after.budgetMs}ms${sized} and ${state}, so it may announce a moment from now: editor_status says whether it has, and editor_run start takes runtimeWaitMs to wait longer than this${after.withArgs ? ", which a run carrying its own arguments may well need, since whatever they ask the game to do before its first frame is inside this wait" : ""}. ${untilItAnnounces(after.servesAProject)}` : `nothing announced itself within ${after.budgetMs}ms${sized} and the game is no longer running, so nothing is going to: editor_output has what it printed on the way down`,
|
|
34415
34520
|
heldAt: null
|
|
34416
34521
|
};
|
|
34417
34522
|
}
|
|
34523
|
+
function untilItAnnounces(servesAProject) {
|
|
34524
|
+
return servesAProject ? "Until it does, a runtime_* call for it waits briefly and is then refused, and another project's game answers only a call whose projectPath names that project." : "Until it does, a runtime_* call naming no projectPath reaches whichever game is the only one announced, another project's included, so pass projectPath to be sure the answer is this game's.";
|
|
34525
|
+
}
|
|
34418
34526
|
function isOfType(value, kind) {
|
|
34419
34527
|
switch (kind) {
|
|
34420
34528
|
case "string":
|
|
@@ -34615,18 +34723,50 @@ function settingsNamedBy(op, args) {
|
|
|
34615
34723
|
}
|
|
34616
34724
|
var STALLED_AFTER_MS = 30000;
|
|
34617
34725
|
var DEPENDED_FAILED = "Failed to compile depended scripts";
|
|
34618
|
-
function
|
|
34726
|
+
function foldedScriptErrors(errors) {
|
|
34727
|
+
const scriptErrors = errors.filter((error) => !error.message.includes(DEPENDED_FAILED));
|
|
34728
|
+
const own = new Set(scriptErrors.map((error) => error.path));
|
|
34729
|
+
return {
|
|
34730
|
+
scriptErrors,
|
|
34731
|
+
failedThroughDependency: [...new Set(errors.map((error) => error.path))].filter((path) => !own.has(path))
|
|
34732
|
+
};
|
|
34733
|
+
}
|
|
34734
|
+
function nonEmpty(lists) {
|
|
34735
|
+
return Object.fromEntries(Object.entries(lists).filter(([, list]) => list.length > 0));
|
|
34736
|
+
}
|
|
34737
|
+
function scriptErrorsNote(errors, {
|
|
34738
|
+
origins,
|
|
34739
|
+
unconfirmed
|
|
34740
|
+
} = {
|
|
34741
|
+
origins: [],
|
|
34742
|
+
unconfirmed: []
|
|
34743
|
+
}) {
|
|
34744
|
+
const { scriptErrors: own, failedThroughDependency: dependents } = foldedScriptErrors(errors);
|
|
34619
34745
|
const causes = new Map;
|
|
34620
|
-
for (const error of
|
|
34621
|
-
if (!
|
|
34746
|
+
for (const error of own) {
|
|
34747
|
+
if (!causes.has(error.path)) {
|
|
34622
34748
|
causes.set(error.path, error);
|
|
34623
34749
|
}
|
|
34624
34750
|
}
|
|
34625
|
-
const dependents = [...new Set(errors.map((error) => error.path))].filter((path) => !causes.has(path));
|
|
34626
34751
|
const scripts = causes.size + dependents.length;
|
|
34627
|
-
const
|
|
34752
|
+
const at = (error) => error.line === null ? error.path : `${error.path}:${error.line}`;
|
|
34753
|
+
const sentence = (text) => `${text}${/[.!?]\)?$/.test(text) ? "" : "."}`;
|
|
34754
|
+
const traced = new Set;
|
|
34755
|
+
const failedOn = (declares) => {
|
|
34756
|
+
const through = [...causes.values()].filter((error) => unresolvedClassIn(error.message) === declares);
|
|
34757
|
+
for (const error of through) {
|
|
34758
|
+
traced.add(error.path);
|
|
34759
|
+
}
|
|
34760
|
+
return through.length === 0 ? "" : ` ${through.map(at).join(", ")} could not resolve ${declares} because of it.`;
|
|
34761
|
+
};
|
|
34762
|
+
const first = [...new Map(origins.map((origin) => [origin.path, origin])).values()].sort((a, b) => Number(unresolvedClassIn(a.message) !== null) - Number(unresolvedClassIn(b.message) !== null));
|
|
34763
|
+
const named = [
|
|
34764
|
+
...first.map((origin) => `${at(origin)} ${sentence(origin.message)} gdUnit4 did not name this script, which declares ${origin.declares}; the engine's own check of it found the error.${failedOn(origin.declares)}`),
|
|
34765
|
+
...unconfirmed.map((origin) => `${origin.path} declares ${origin.declares}, which gdUnit4 did not name, and ${origin.why}.${failedOn(origin.declares)}`),
|
|
34766
|
+
...[...causes.values()].filter((error) => !traced.has(error.path)).map((error) => `${at(error)} ${sentence(error.message)}`)
|
|
34767
|
+
].join(" ");
|
|
34628
34768
|
const following = dependents.length === 0 ? "" : causes.size === 0 ? `${dependents.join(", ")} ${dependents.length === 1 ? "depends" : "depend"} on a script that did not compile, and gdUnit4 named none with an error of its own: script_diagnostics on one of them finds it.` : `${dependents.length === 1 ? "One more" : `${dependents.length} more`} failed only because a script ${dependents.length === 1 ? "it depends" : "they depend"} on did: ${dependents.join(", ")}.`;
|
|
34629
|
-
const more =
|
|
34769
|
+
const more = own.length - causes.size;
|
|
34630
34770
|
const rest = more > 0 ? `${more === 1 ? "One more error" : `${more} more errors`} in ${causes.size === 1 ? "that script" : "those scripts"} ${more === 1 ? "is" : "are"} under scriptErrors.` : "";
|
|
34631
34771
|
return [
|
|
34632
34772
|
`No tests ran: gdUnit4 could not load ${scripts === 1 ? "one script" : `${scripts} scripts`} while looking for suites, and it runs no suite at all when one fails to load.`,
|
|
@@ -34723,7 +34863,7 @@ function aboveTheRunner(entry) {
|
|
|
34723
34863
|
}
|
|
34724
34864
|
function elsewhereIn(projectPath, asked) {
|
|
34725
34865
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
34726
|
-
if (wanted === "" || existsSync16(
|
|
34866
|
+
if (wanted === "" || existsSync16(join21(projectPath, wanted))) {
|
|
34727
34867
|
return "";
|
|
34728
34868
|
}
|
|
34729
34869
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -34775,7 +34915,7 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
34775
34915
|
return false;
|
|
34776
34916
|
}
|
|
34777
34917
|
try {
|
|
34778
|
-
return readFileSync19(
|
|
34918
|
+
return readFileSync19(join21(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
34779
34919
|
} catch {
|
|
34780
34920
|
return false;
|
|
34781
34921
|
}
|
|
@@ -34800,7 +34940,7 @@ function needsItsOwnProcess(asked) {
|
|
|
34800
34940
|
class GodotServer {
|
|
34801
34941
|
mcp;
|
|
34802
34942
|
locator = new GodotLocator;
|
|
34803
|
-
operationsScript =
|
|
34943
|
+
operationsScript = join21(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
34804
34944
|
godotBridge;
|
|
34805
34945
|
tools = buildToolDefinitions();
|
|
34806
34946
|
activeProcess = null;
|
|
@@ -35103,7 +35243,7 @@ class GodotServer {
|
|
|
35103
35243
|
return checked.response;
|
|
35104
35244
|
}
|
|
35105
35245
|
const named = args["projectPath"];
|
|
35106
|
-
if (typeof named === "string" && existsSync16(
|
|
35246
|
+
if (typeof named === "string" && existsSync16(join21(named, "project.godot"))) {
|
|
35107
35247
|
this.lastProjectPath = named;
|
|
35108
35248
|
}
|
|
35109
35249
|
this.updates.refresh();
|
|
@@ -35491,7 +35631,8 @@ class GodotServer {
|
|
|
35491
35631
|
...whichGame(args),
|
|
35492
35632
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
35493
35633
|
method: readString(args, "method") ?? "",
|
|
35494
|
-
args: readArray(args, "args") ?? []
|
|
35634
|
+
args: readArray(args, "args") ?? [],
|
|
35635
|
+
...args["properties"] === undefined ? {} : { properties: args["properties"] }
|
|
35495
35636
|
}, patience);
|
|
35496
35637
|
}
|
|
35497
35638
|
case "runtime_capture":
|
|
@@ -35576,7 +35717,7 @@ class GodotServer {
|
|
|
35576
35717
|
if (path === undefined) {
|
|
35577
35718
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
35578
35719
|
}
|
|
35579
|
-
const file =
|
|
35720
|
+
const file = join21(path, "project.godot");
|
|
35580
35721
|
if (!existsSync16(file)) {
|
|
35581
35722
|
return {
|
|
35582
35723
|
ok: false,
|
|
@@ -36255,7 +36396,7 @@ class GodotServer {
|
|
|
36255
36396
|
const exportArgs = [
|
|
36256
36397
|
"--headless",
|
|
36257
36398
|
"--log-file",
|
|
36258
|
-
|
|
36399
|
+
join21(exportLogs, "engine.log"),
|
|
36259
36400
|
"--path",
|
|
36260
36401
|
project.value.path,
|
|
36261
36402
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -36285,9 +36426,9 @@ class GodotServer {
|
|
|
36285
36426
|
if (!paths.ok) {
|
|
36286
36427
|
return this.createErrorResponse(paths.reason, PATH_SOLUTIONS);
|
|
36287
36428
|
}
|
|
36288
|
-
const
|
|
36289
|
-
const
|
|
36290
|
-
if (
|
|
36429
|
+
const missing = paths.value.filter((path) => !existsSync16(join21(project.value.path, path.slice("res://".length))));
|
|
36430
|
+
const asked = paths.value.filter((path) => !missing.includes(path));
|
|
36431
|
+
if (asked.length === 0) {
|
|
36291
36432
|
const verdict = `nothing at ${missing.join(" and ")}`;
|
|
36292
36433
|
return {
|
|
36293
36434
|
content: [
|
|
@@ -36301,7 +36442,7 @@ class GodotServer {
|
|
|
36301
36442
|
};
|
|
36302
36443
|
}
|
|
36303
36444
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
36304
|
-
if (!existsSync16(
|
|
36445
|
+
if (!existsSync16(join21(project.value.path, runner))) {
|
|
36305
36446
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
36306
36447
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
36307
36448
|
]);
|
|
@@ -36366,14 +36507,14 @@ class GodotServer {
|
|
|
36366
36507
|
await untilGone([run.process.pid]);
|
|
36367
36508
|
}
|
|
36368
36509
|
await run.settled;
|
|
36369
|
-
const reportsDir =
|
|
36510
|
+
const reportsDir = join21(project.value.path, ".godot", ...ours.split("/"));
|
|
36370
36511
|
let report = null;
|
|
36371
36512
|
let reportProblem = null;
|
|
36372
36513
|
try {
|
|
36373
36514
|
const written = existsSync16(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
36374
36515
|
const newest = written.at(-1);
|
|
36375
36516
|
if (newest !== undefined) {
|
|
36376
|
-
report = parseJUnit(readFileSync19(
|
|
36517
|
+
report = parseJUnit(readFileSync19(join21(reportsDir, newest, "results.xml"), "utf8"));
|
|
36377
36518
|
}
|
|
36378
36519
|
} catch (error) {
|
|
36379
36520
|
reportProblem = errorMessage(error);
|
|
@@ -36402,9 +36543,14 @@ class GodotServer {
|
|
|
36402
36543
|
const nothingRan = timedOut ? null : whyNoReport(said, asked.join(" and "));
|
|
36403
36544
|
const cutShort = timedOut ? timedOutVerdict(timeoutMs, silentForMs) : null;
|
|
36404
36545
|
const hung = cutShort?.hung ?? false;
|
|
36405
|
-
const
|
|
36546
|
+
const printedErrors = timedOut || exitCode !== 105 ? [] : scriptErrorsPrinted(said);
|
|
36547
|
+
const declaring = printedErrors.some((error) => unresolvedClassIn(error.message) !== null) ? await originsOf(printedErrors, declaredClasses(project.value.path), checkWithEngine(engine.value, project.value.path)) : { origins: [], unconfirmed: [] };
|
|
36548
|
+
const scriptErrors = [...declaring.origins, ...printedErrors];
|
|
36406
36549
|
const brokenScripts = new Set(scriptErrors.map((error) => error.path)).size;
|
|
36407
|
-
const
|
|
36550
|
+
const ranAs = cutShort !== null ? cutShort.verdict : nothingRan ?? (brokenScripts > 0 ? `script errors in ${brokenScripts === 1 ? "one script" : `${brokenScripts} scripts`}, so no suite ran` : undefined) ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
|
|
36551
|
+
const nothingAt = missing.join(" and ");
|
|
36552
|
+
const verdict = missing.length === 0 ? ranAs : `${ranAs}; nothing at ${nothingAt}`;
|
|
36553
|
+
const missingNote = missing.length === 0 ? undefined : `Nothing at ${nothingAt}, so ${missing.length === 1 ? "it was" : "they were"} left out and only ${asked.join(", ")} ran; a run of fewer paths than were asked for is not called a pass.${elsewhereIn(project.value.path, missing[0] ?? "")}`;
|
|
36408
36554
|
const savesNote = savesStayPut() && nothingRan === null ? SAVES_NOT_MOVED_NOTE : undefined;
|
|
36409
36555
|
const marker = readNonEmptyString(args, "printed");
|
|
36410
36556
|
const echoed = marker === undefined ? undefined : run.log.select({ severity: "info", sinceLastCall: false, contains: marker, limit: 200 });
|
|
@@ -36413,7 +36559,8 @@ class GodotServer {
|
|
|
36413
36559
|
...echoed.omitted > 0 ? { printedOmitted: echoed.omitted } : {}
|
|
36414
36560
|
};
|
|
36415
36561
|
if (report === null) {
|
|
36416
|
-
const
|
|
36562
|
+
const said = nothingRan !== null ? `No tests ran: ${ranAs}.` : scriptErrors.length > 0 ? scriptErrorsNote(printedErrors, declaring) : reportProblem !== null ? `The test run's report could not be read (${reportProblem}); the engine's own verdict was ${ranAs}.` : `The test run wrote no report (${ranAs}).`;
|
|
36563
|
+
const note = missingNote === undefined ? said : `${said} ${missingNote}`;
|
|
36417
36564
|
return {
|
|
36418
36565
|
content: [
|
|
36419
36566
|
{ type: "text", text: note },
|
|
@@ -36427,9 +36574,10 @@ class GodotServer {
|
|
|
36427
36574
|
exitSignal: run.exitSignal ?? undefined,
|
|
36428
36575
|
hung,
|
|
36429
36576
|
...timedOut ? { timedOut, silentForMs } : {},
|
|
36430
|
-
...
|
|
36577
|
+
...missing.length > 0 ? { missing } : {},
|
|
36578
|
+
...nonEmpty(foldedScriptErrors(scriptErrors)),
|
|
36431
36579
|
arguments: cmdArgs,
|
|
36432
|
-
entries: forAnswer(printed.slice(0, 60).map(aboveTheRunner)),
|
|
36580
|
+
entries: forAnswer(withoutListed(printed, scriptErrors).slice(0, 60).map(aboveTheRunner)),
|
|
36433
36581
|
...printedLines,
|
|
36434
36582
|
savesNote,
|
|
36435
36583
|
...scanned
|
|
@@ -36464,8 +36612,9 @@ class GodotServer {
|
|
|
36464
36612
|
});
|
|
36465
36613
|
const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
|
|
36466
36614
|
return this.jsonTextResponse({
|
|
36467
|
-
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0,
|
|
36615
|
+
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && missing.length === 0,
|
|
36468
36616
|
verdict,
|
|
36617
|
+
...missing.length > 0 ? { missing } : {},
|
|
36469
36618
|
...timedOut ? { timedOut, hung, silentForMs } : {},
|
|
36470
36619
|
exitCode,
|
|
36471
36620
|
exitSignal: run.exitSignal ?? undefined,
|
|
@@ -36482,7 +36631,10 @@ class GodotServer {
|
|
|
36482
36631
|
...printedLines,
|
|
36483
36632
|
savesNote,
|
|
36484
36633
|
...scanned,
|
|
36485
|
-
note:
|
|
36634
|
+
note: [
|
|
36635
|
+
missingNote,
|
|
36636
|
+
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)
|
|
36637
|
+
].filter((part) => part !== undefined).join(" ") || undefined,
|
|
36486
36638
|
suites: unclean.map((suite) => ({
|
|
36487
36639
|
name: suite.name,
|
|
36488
36640
|
path: suite.path,
|
|
@@ -36933,7 +37085,7 @@ class GodotServer {
|
|
|
36933
37085
|
return null;
|
|
36934
37086
|
}
|
|
36935
37087
|
try {
|
|
36936
|
-
return settingKeys(readFileSync19(
|
|
37088
|
+
return settingKeys(readFileSync19(join21(projectPath, "project.godot"), "utf8"));
|
|
36937
37089
|
} catch {
|
|
36938
37090
|
return null;
|
|
36939
37091
|
}
|
|
@@ -37255,8 +37407,15 @@ class GodotServer {
|
|
|
37255
37407
|
}
|
|
37256
37408
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
37257
37409
|
const budgetMs = budget.ms;
|
|
37258
|
-
if (!existsSync16(
|
|
37259
|
-
return runtimeVerdict(null, {
|
|
37410
|
+
if (!existsSync16(join21(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
37411
|
+
return runtimeVerdict(null, {
|
|
37412
|
+
addon: false,
|
|
37413
|
+
budgetMs,
|
|
37414
|
+
heldAt: null,
|
|
37415
|
+
running: false,
|
|
37416
|
+
withArgs,
|
|
37417
|
+
servesAProject: this.ownProject !== null
|
|
37418
|
+
});
|
|
37260
37419
|
}
|
|
37261
37420
|
let asking = null;
|
|
37262
37421
|
let editorAskedAt = 0;
|
|
@@ -37308,6 +37467,7 @@ class GodotServer {
|
|
|
37308
37467
|
heldAt: this.dapClient?.whereItStopped() ?? null,
|
|
37309
37468
|
running: endpoint !== null || going !== null && await this.runStillGoing(going),
|
|
37310
37469
|
withArgs,
|
|
37470
|
+
servesAProject: this.ownProject !== null,
|
|
37311
37471
|
...going?.throughEditor === true ? { playingSeen: going.seenPlaying === true } : {}
|
|
37312
37472
|
});
|
|
37313
37473
|
}
|
|
@@ -38264,7 +38424,7 @@ class GodotServer {
|
|
|
38264
38424
|
notes.push(endedWithoutACode({
|
|
38265
38425
|
throughEditor: run.throughEditor,
|
|
38266
38426
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
38267
|
-
couldAnnounce: run.projectPath !== null && existsSync16(
|
|
38427
|
+
couldAnnounce: run.projectPath !== null && existsSync16(join21(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
38268
38428
|
endedHere: typeof run.endedHere === "string",
|
|
38269
38429
|
pickedUp: run.pickedUp === true
|
|
38270
38430
|
}));
|
|
@@ -39113,7 +39273,7 @@ class GodotServer {
|
|
|
39113
39273
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
39114
39274
|
const run = await this.currentRun();
|
|
39115
39275
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
39116
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync16(
|
|
39276
|
+
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync16(join21(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
|
|
39117
39277
|
return null;
|
|
39118
39278
|
}
|
|
39119
39279
|
const already = runtimesAnnounced().running;
|
|
@@ -39197,7 +39357,7 @@ class GodotServer {
|
|
|
39197
39357
|
}
|
|
39198
39358
|
}
|
|
39199
39359
|
const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
|
|
39200
|
-
const screenshotPath = screenshotDir ?
|
|
39360
|
+
const screenshotPath = screenshotDir ? join21(screenshotDir, "capture.png") : null;
|
|
39201
39361
|
const staleNote = this.runtimeStaleNote(choice.endpoint);
|
|
39202
39362
|
const stale = staleNote === undefined ? {} : { addonIsStale: true, staleNote };
|
|
39203
39363
|
const refused = (message) => this.createErrorResponse(staleNote === undefined ? message : `${message}
|