gdharness 1.1.46 → 1.1.48
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 +479 -157
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +11 -5
- package/build/godot/addons/gdharness_runtime/runtime_menus.gd +1 -1
- package/build/godot/addons/gdharness_runtime/runtime_says.gd +63 -4
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +100 -15
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +8 -1
- package/build/index.js +520 -205
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -10249,17 +10249,17 @@ import {
|
|
|
10249
10249
|
closeSync as closeSync2,
|
|
10250
10250
|
constants,
|
|
10251
10251
|
copyFileSync,
|
|
10252
|
-
existsSync as
|
|
10252
|
+
existsSync as existsSync17,
|
|
10253
10253
|
fstatSync,
|
|
10254
|
-
mkdirSync as
|
|
10254
|
+
mkdirSync as mkdirSync13,
|
|
10255
10255
|
openSync as openSync3,
|
|
10256
10256
|
readdirSync as readdirSync9,
|
|
10257
|
-
readFileSync as
|
|
10257
|
+
readFileSync as readFileSync20,
|
|
10258
10258
|
readSync as readSync2,
|
|
10259
10259
|
rmdirSync as rmdirSync2,
|
|
10260
10260
|
statSync as statSync8
|
|
10261
10261
|
} from "node:fs";
|
|
10262
|
-
import { basename as basename5, dirname as
|
|
10262
|
+
import { basename as basename5, dirname as dirname15, extname as extname2, isAbsolute as isAbsolute3, join as join22, 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";
|
|
@@ -26178,11 +26178,14 @@ function withdrawBridge(path) {
|
|
|
26178
26178
|
// src/call-signal.ts
|
|
26179
26179
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
26180
26180
|
var current = new AsyncLocalStorage;
|
|
26181
|
-
function withCallSignal(signal, answer) {
|
|
26182
|
-
return signal === undefined ? answer() : current.run(signal, answer);
|
|
26181
|
+
function withCallSignal(signal, answer, progress) {
|
|
26182
|
+
return signal === undefined && progress === undefined ? answer() : current.run({ signal, progress }, answer);
|
|
26183
26183
|
}
|
|
26184
26184
|
function callSignal() {
|
|
26185
|
-
return current.getStore();
|
|
26185
|
+
return current.getStore()?.signal;
|
|
26186
|
+
}
|
|
26187
|
+
function callProgress() {
|
|
26188
|
+
return current.getStore()?.progress;
|
|
26186
26189
|
}
|
|
26187
26190
|
|
|
26188
26191
|
// src/class-cache.ts
|
|
@@ -26945,6 +26948,41 @@ function configDisagrees(projectPath, serverVersion) {
|
|
|
26945
26948
|
return `.mcp.json in this project names gdharness@${others.join(" and gdharness@")}, and this server ` + `is ${serverVersion}. Whichever is meant to be right, nothing will change version until the ` + "MCP server is reconnected, and a pin kept anywhere else does not reach this file.";
|
|
26946
26949
|
}
|
|
26947
26950
|
|
|
26951
|
+
// src/cut-test-run.ts
|
|
26952
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync10, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
|
|
26953
|
+
import { dirname as dirname6, join as join9 } from "node:path";
|
|
26954
|
+
function keptAt(projectPath) {
|
|
26955
|
+
return join9(projectPath, ".godot", "gdharness-reports", "cut-run.json");
|
|
26956
|
+
}
|
|
26957
|
+
function keepCutTestRun(projectPath, run) {
|
|
26958
|
+
const path = keptAt(projectPath);
|
|
26959
|
+
try {
|
|
26960
|
+
mkdirSync6(dirname6(path), { recursive: true });
|
|
26961
|
+
writeFileSync6(path, `${JSON.stringify(run, null, 2)}
|
|
26962
|
+
`, "utf8");
|
|
26963
|
+
} catch {}
|
|
26964
|
+
}
|
|
26965
|
+
function takeCutTestRun(projectPath) {
|
|
26966
|
+
const path = keptAt(projectPath);
|
|
26967
|
+
if (!existsSync5(path)) {
|
|
26968
|
+
return null;
|
|
26969
|
+
}
|
|
26970
|
+
try {
|
|
26971
|
+
const run = JSON.parse(readFileSync10(path, "utf8"));
|
|
26972
|
+
rmSync3(path, { force: true });
|
|
26973
|
+
return typeof run.cutAt === "string" && Array.isArray(run.paths) && run.paths.every((path) => typeof path === "string") ? run : null;
|
|
26974
|
+
} catch {
|
|
26975
|
+
rmSync3(path, { force: true });
|
|
26976
|
+
return null;
|
|
26977
|
+
}
|
|
26978
|
+
}
|
|
26979
|
+
function cutTestRunNote(run) {
|
|
26980
|
+
const finished = run.suitesFinished?.length ?? 0;
|
|
26981
|
+
const failing = run.suitesFailing?.length ?? 0;
|
|
26982
|
+
const got = finished === 0 ? "before any suite had finished" : `after ${finished === 1 ? "one suite" : `${finished} suites`} had finished${failing === 0 ? "" : finished === 1 ? ", with failures" : `, ${failing} of them with failures`}`;
|
|
26983
|
+
return `The test run before this one in this project, of ${run.paths.join(", ")}, was cut off by its client at ${run.cutAt} ${got}${run.suiteRunning === undefined ? "" : `, while ${run.suiteRunning} was running`}; what it finished is under previousRunCut.`;
|
|
26984
|
+
}
|
|
26985
|
+
|
|
26948
26986
|
// src/dap_client.ts
|
|
26949
26987
|
import { createConnection as createConnection2 } from "node:net";
|
|
26950
26988
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
@@ -27737,16 +27775,16 @@ function dictionary(entries) {
|
|
|
27737
27775
|
// src/editor-log.ts
|
|
27738
27776
|
import {
|
|
27739
27777
|
closeSync,
|
|
27740
|
-
existsSync as
|
|
27741
|
-
mkdirSync as
|
|
27778
|
+
existsSync as existsSync6,
|
|
27779
|
+
mkdirSync as mkdirSync7,
|
|
27742
27780
|
openSync,
|
|
27743
|
-
readFileSync as
|
|
27781
|
+
readFileSync as readFileSync11,
|
|
27744
27782
|
readSync,
|
|
27745
|
-
rmSync as
|
|
27783
|
+
rmSync as rmSync4,
|
|
27746
27784
|
statSync as statSync3,
|
|
27747
|
-
writeFileSync as
|
|
27785
|
+
writeFileSync as writeFileSync7
|
|
27748
27786
|
} from "node:fs";
|
|
27749
|
-
import { dirname as
|
|
27787
|
+
import { dirname as dirname7, join as join10 } from "node:path";
|
|
27750
27788
|
|
|
27751
27789
|
// src/game-log.ts
|
|
27752
27790
|
import { StringDecoder } from "node:string_decoder";
|
|
@@ -27864,14 +27902,14 @@ function answersTo(entry, severity, contains) {
|
|
|
27864
27902
|
|
|
27865
27903
|
// src/editor-log.ts
|
|
27866
27904
|
function editorLogPath(projectPath) {
|
|
27867
|
-
return
|
|
27905
|
+
return join10(projectPath, ".godot", "gdharness-editor.log");
|
|
27868
27906
|
}
|
|
27869
27907
|
function editorLogNotePath(projectPath) {
|
|
27870
|
-
return
|
|
27908
|
+
return join10(projectPath, ".godot", "gdharness-editor.json");
|
|
27871
27909
|
}
|
|
27872
27910
|
function readEditorLogNote(projectPath) {
|
|
27873
27911
|
try {
|
|
27874
|
-
const fields = asParams(JSON.parse(
|
|
27912
|
+
const fields = asParams(JSON.parse(readFileSync11(editorLogNotePath(projectPath), "utf8")));
|
|
27875
27913
|
const pid = readNumber(fields, "pid");
|
|
27876
27914
|
const startedAt = readString(fields, "startedAt");
|
|
27877
27915
|
return pid !== undefined && Number.isInteger(pid) && pid > 0 && startedAt !== undefined ? { pid, startedAt } : null;
|
|
@@ -27882,16 +27920,16 @@ function readEditorLogNote(projectPath) {
|
|
|
27882
27920
|
function clearEditorLog(projectPath) {
|
|
27883
27921
|
try {
|
|
27884
27922
|
const path = editorLogPath(projectPath);
|
|
27885
|
-
|
|
27886
|
-
|
|
27887
|
-
|
|
27923
|
+
mkdirSync7(dirname7(path), { recursive: true });
|
|
27924
|
+
rmSync4(path, { force: true });
|
|
27925
|
+
rmSync4(editorLogNotePath(projectPath), { force: true });
|
|
27888
27926
|
} catch {}
|
|
27889
27927
|
}
|
|
27890
27928
|
function writeEditorLogNote(projectPath, pid) {
|
|
27891
27929
|
try {
|
|
27892
27930
|
const path = editorLogNotePath(projectPath);
|
|
27893
|
-
|
|
27894
|
-
|
|
27931
|
+
mkdirSync7(dirname7(path), { recursive: true });
|
|
27932
|
+
writeFileSync7(path, JSON.stringify({ pid, startedAt: new Date().toISOString() }, null, 2), "utf8");
|
|
27895
27933
|
} catch {}
|
|
27896
27934
|
}
|
|
27897
27935
|
function theConsoleWasNotCaptured(why, projectPath) {
|
|
@@ -27912,7 +27950,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
|
|
|
27912
27950
|
return { kind: "older server", notedPid: noted?.pid ?? null };
|
|
27913
27951
|
}
|
|
27914
27952
|
const path = editorLogPath(projectPath);
|
|
27915
|
-
if (!
|
|
27953
|
+
if (!existsSync6(path)) {
|
|
27916
27954
|
return { kind: "not written" };
|
|
27917
27955
|
}
|
|
27918
27956
|
const log = new GameLog;
|
|
@@ -27925,7 +27963,7 @@ var MOST_CONSOLE_BYTES = 8 * 1024 * 1024;
|
|
|
27925
27963
|
function theFirstOf(path) {
|
|
27926
27964
|
const whole = statSync3(path).size;
|
|
27927
27965
|
if (whole <= MOST_CONSOLE_BYTES) {
|
|
27928
|
-
return { text:
|
|
27966
|
+
return { text: readFileSync11(path, "utf8") };
|
|
27929
27967
|
}
|
|
27930
27968
|
const handle = openSync(path, "r");
|
|
27931
27969
|
try {
|
|
@@ -28693,14 +28731,14 @@ function getDefaultBridge(ownProject = null) {
|
|
|
28693
28731
|
|
|
28694
28732
|
// src/godot-path.ts
|
|
28695
28733
|
import { execFile as execFile2 } from "node:child_process";
|
|
28696
|
-
import { existsSync as
|
|
28734
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
28697
28735
|
import { normalize } from "node:path";
|
|
28698
28736
|
import { promisify as promisify2 } from "node:util";
|
|
28699
28737
|
|
|
28700
28738
|
// src/detection.ts
|
|
28701
|
-
import { existsSync as
|
|
28739
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
|
|
28702
28740
|
import { homedir as homedir2 } from "node:os";
|
|
28703
|
-
import { join as
|
|
28741
|
+
import { join as join11 } from "node:path";
|
|
28704
28742
|
function resolveHomeDirectory() {
|
|
28705
28743
|
try {
|
|
28706
28744
|
return homedir2();
|
|
@@ -28709,7 +28747,7 @@ function resolveHomeDirectory() {
|
|
|
28709
28747
|
}
|
|
28710
28748
|
}
|
|
28711
28749
|
function scanDirectoryForGodotBinaries(directory, platform) {
|
|
28712
|
-
if (!directory || !
|
|
28750
|
+
if (!directory || !existsSync7(directory)) {
|
|
28713
28751
|
return [];
|
|
28714
28752
|
}
|
|
28715
28753
|
let entries;
|
|
@@ -28724,7 +28762,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
|
|
|
28724
28762
|
if (!pattern.test(name)) {
|
|
28725
28763
|
continue;
|
|
28726
28764
|
}
|
|
28727
|
-
const fullPath =
|
|
28765
|
+
const fullPath = join11(directory, name);
|
|
28728
28766
|
try {
|
|
28729
28767
|
const stat = statSync4(fullPath);
|
|
28730
28768
|
if (stat.isFile()) {
|
|
@@ -28733,7 +28771,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
|
|
|
28733
28771
|
} catch {}
|
|
28734
28772
|
}
|
|
28735
28773
|
matches.sort((a, b) => b.mtime - a.mtime);
|
|
28736
|
-
return matches.map((m) =>
|
|
28774
|
+
return matches.map((m) => join11(directory, m.name));
|
|
28737
28775
|
}
|
|
28738
28776
|
function conventionalPaths(platform, home) {
|
|
28739
28777
|
const paths = ["godot"];
|
|
@@ -28836,7 +28874,7 @@ class GodotLocator {
|
|
|
28836
28874
|
return known;
|
|
28837
28875
|
}
|
|
28838
28876
|
let reason;
|
|
28839
|
-
if (path !== "godot" && !
|
|
28877
|
+
if (path !== "godot" && !existsSync8(path)) {
|
|
28840
28878
|
reason = "does not exist";
|
|
28841
28879
|
} else {
|
|
28842
28880
|
try {
|
|
@@ -28852,33 +28890,33 @@ class GodotLocator {
|
|
|
28852
28890
|
}
|
|
28853
28891
|
|
|
28854
28892
|
// src/headless.ts
|
|
28855
|
-
import { existsSync as
|
|
28856
|
-
import { dirname as
|
|
28893
|
+
import { existsSync as existsSync11, readFileSync as readFileSync13, realpathSync as realpathSync2, writeFileSync as writeFileSync9 } from "node:fs";
|
|
28894
|
+
import { dirname as dirname8, join as join14 } from "node:path";
|
|
28857
28895
|
|
|
28858
28896
|
// src/reimport.ts
|
|
28859
28897
|
import { createHash as createHash2 } from "node:crypto";
|
|
28860
|
-
import { existsSync as
|
|
28861
|
-
import { basename as basename2, join as
|
|
28898
|
+
import { existsSync as existsSync9, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync5, utimesSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
28899
|
+
import { basename as basename2, join as join12 } from "node:path";
|
|
28862
28900
|
function forceNextImport(projectPath, resourcePath) {
|
|
28863
28901
|
const relative = resourcePath.replace(/^res:\/\//, "");
|
|
28864
|
-
const sidecar =
|
|
28865
|
-
if (!
|
|
28902
|
+
const sidecar = join12(projectPath, `${relative}.import`);
|
|
28903
|
+
if (!existsSync9(sidecar)) {
|
|
28866
28904
|
return;
|
|
28867
28905
|
}
|
|
28868
28906
|
const { atime, mtimeMs } = statSync5(sidecar);
|
|
28869
|
-
|
|
28907
|
+
writeFileSync8(sidecar, readFileSync12(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
|
|
28870
28908
|
utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
|
|
28871
|
-
|
|
28909
|
+
rmSync5(importRecord(projectPath, resourcePath), { force: true });
|
|
28872
28910
|
}
|
|
28873
28911
|
function importRecord(projectPath, resourcePath) {
|
|
28874
28912
|
const digest = createHash2("md5").update(resourcePath).digest("hex");
|
|
28875
|
-
return
|
|
28913
|
+
return join12(projectPath, ".godot", "imported", `${basename2(resourcePath)}-${digest}.md5`);
|
|
28876
28914
|
}
|
|
28877
28915
|
|
|
28878
28916
|
// src/scratch.ts
|
|
28879
|
-
import { existsSync as
|
|
28917
|
+
import { existsSync as existsSync10, mkdtempSync, readdirSync as readdirSync4, rmSync as rmSync6 } from "node:fs";
|
|
28880
28918
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
28881
|
-
import { join as
|
|
28919
|
+
import { join as join13 } from "node:path";
|
|
28882
28920
|
import process7 from "node:process";
|
|
28883
28921
|
var SWEPT = { recursive: true, force: true };
|
|
28884
28922
|
var AGAIN_AFTER_MS = [250, 500, 1000, 2000, 4000, 8000, 16000];
|
|
@@ -28893,7 +28931,7 @@ var KINDS = [
|
|
|
28893
28931
|
"tests"
|
|
28894
28932
|
];
|
|
28895
28933
|
function scratchDirectory(kind) {
|
|
28896
|
-
return mkdtempSync(
|
|
28934
|
+
return mkdtempSync(join13(tmpdir3(), `gdharness-${kind}-${process7.pid}-`));
|
|
28897
28935
|
}
|
|
28898
28936
|
function sweepAbandonedScratch(directory = tmpdir3()) {
|
|
28899
28937
|
let entries;
|
|
@@ -28909,7 +28947,7 @@ function sweepAbandonedScratch(directory = tmpdir3()) {
|
|
|
28909
28947
|
continue;
|
|
28910
28948
|
}
|
|
28911
28949
|
try {
|
|
28912
|
-
remove(
|
|
28950
|
+
remove(join13(directory, entry));
|
|
28913
28951
|
} catch {}
|
|
28914
28952
|
}
|
|
28915
28953
|
}
|
|
@@ -28935,7 +28973,7 @@ function discard(...paths) {
|
|
|
28935
28973
|
return left;
|
|
28936
28974
|
}
|
|
28937
28975
|
function remove(path) {
|
|
28938
|
-
|
|
28976
|
+
rmSync6(path, SWEPT);
|
|
28939
28977
|
}
|
|
28940
28978
|
function tryAgain(path, attempt) {
|
|
28941
28979
|
if (attempt === 0) {
|
|
@@ -28947,7 +28985,7 @@ function tryAgain(path, attempt) {
|
|
|
28947
28985
|
lastTryOnExit();
|
|
28948
28986
|
}
|
|
28949
28987
|
const after = AGAIN_AFTER_MS[attempt];
|
|
28950
|
-
if (after === undefined || !
|
|
28988
|
+
if (after === undefined || !existsSync10(path)) {
|
|
28951
28989
|
waiting.delete(path);
|
|
28952
28990
|
if (after === undefined) {
|
|
28953
28991
|
atExit.add(path);
|
|
@@ -29027,7 +29065,7 @@ function reason(log, problems) {
|
|
|
29027
29065
|
}
|
|
29028
29066
|
function scriptFault(problems, script) {
|
|
29029
29067
|
const spelled = (path) => `(${path.replaceAll("\\", "/").toLowerCase()}/`;
|
|
29030
|
-
const directories = [...new Set([spelled(
|
|
29068
|
+
const directories = [...new Set([spelled(dirname8(script)), spelled(resolvedOr(dirname8(script)))])];
|
|
29031
29069
|
return problems.find((entry) => {
|
|
29032
29070
|
const at = entry.detail.find((line) => line.startsWith("at: "))?.replaceAll("\\", "/").toLowerCase();
|
|
29033
29071
|
return entry.severity === "error" && at !== undefined && directories.some((directory) => at.includes(directory));
|
|
@@ -29083,7 +29121,7 @@ async function importOnce(godotPath, projectPath) {
|
|
|
29083
29121
|
ran = await runEngine(godotPath, [
|
|
29084
29122
|
"--headless",
|
|
29085
29123
|
"--log-file",
|
|
29086
|
-
|
|
29124
|
+
join14(logDir, "engine.log"),
|
|
29087
29125
|
"--path",
|
|
29088
29126
|
projectPath,
|
|
29089
29127
|
"--import"
|
|
@@ -29103,13 +29141,13 @@ async function importOnce(godotPath, projectPath) {
|
|
|
29103
29141
|
}
|
|
29104
29142
|
async function runOperation(engine, operation, params, projectPath) {
|
|
29105
29143
|
const paramsDir = scratchDirectory("params");
|
|
29106
|
-
const paramsFile =
|
|
29107
|
-
const answerFile =
|
|
29108
|
-
|
|
29144
|
+
const paramsFile = join14(paramsDir, `${operation}.json`);
|
|
29145
|
+
const answerFile = join14(paramsDir, "answer.json");
|
|
29146
|
+
writeFileSync9(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
|
|
29109
29147
|
const args = [
|
|
29110
29148
|
"--headless",
|
|
29111
29149
|
"--log-file",
|
|
29112
|
-
|
|
29150
|
+
join14(paramsDir, "engine.log"),
|
|
29113
29151
|
"--path",
|
|
29114
29152
|
projectPath,
|
|
29115
29153
|
"--script",
|
|
@@ -29160,10 +29198,10 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
29160
29198
|
};
|
|
29161
29199
|
}
|
|
29162
29200
|
function answerIn(file) {
|
|
29163
|
-
if (!
|
|
29201
|
+
if (!existsSync11(file)) {
|
|
29164
29202
|
return { kind: "none" };
|
|
29165
29203
|
}
|
|
29166
|
-
const text =
|
|
29204
|
+
const text = readFileSync13(file, "utf8");
|
|
29167
29205
|
try {
|
|
29168
29206
|
const parsed = JSON.parse(text);
|
|
29169
29207
|
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)}` };
|
|
@@ -29420,12 +29458,16 @@ function whyNoReport(printed, asked) {
|
|
|
29420
29458
|
}
|
|
29421
29459
|
var DISCOVERY_FAILED = "Script errors were detected during test discovery!";
|
|
29422
29460
|
function scriptErrorsPrinted(printed) {
|
|
29461
|
+
return discoveryListing(printed)?.errors ?? [];
|
|
29462
|
+
}
|
|
29463
|
+
function discoveryListing(printed) {
|
|
29423
29464
|
const start = printed.findIndex((line) => line.includes(DISCOVERY_FAILED));
|
|
29424
29465
|
if (start < 0) {
|
|
29425
|
-
return
|
|
29466
|
+
return null;
|
|
29426
29467
|
}
|
|
29427
29468
|
const found = [];
|
|
29428
29469
|
let message = null;
|
|
29470
|
+
let to = start + 1;
|
|
29429
29471
|
for (const line of printed.slice(start + 1)) {
|
|
29430
29472
|
const at = /^\s*at (res:\/\/.+):(\d+)\s*$/.exec(line);
|
|
29431
29473
|
if (at !== null && message !== null) {
|
|
@@ -29436,8 +29478,10 @@ function scriptErrorsPrinted(printed) {
|
|
|
29436
29478
|
} else {
|
|
29437
29479
|
break;
|
|
29438
29480
|
}
|
|
29481
|
+
to += 1;
|
|
29439
29482
|
}
|
|
29440
|
-
|
|
29483
|
+
const errors = found.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : (a.line ?? 0) - (b.line ?? 0));
|
|
29484
|
+
return { errors, from: start, to };
|
|
29441
29485
|
}
|
|
29442
29486
|
var ESCAPE = String.fromCharCode(27);
|
|
29443
29487
|
var LOCATION = `'[^'\\n]*' in \\S+:\\d+`;
|
|
@@ -29573,6 +29617,52 @@ function orphansPrinted(printed) {
|
|
|
29573
29617
|
suites
|
|
29574
29618
|
};
|
|
29575
29619
|
}
|
|
29620
|
+
var SUITE_COUNTS = /(\d+)\s+test cases\s*\|\s*(\d+)\s+errors\s*\|\s*(\d+)\s+failures/;
|
|
29621
|
+
var CASE_FAILED = /^\s*res:\/\/.+?\.(?:gd|cs) > (.+?) (?:FAILED|ERROR)(?:\s|$)/;
|
|
29622
|
+
function finishesASuite(line) {
|
|
29623
|
+
return SUITE_COUNTS.test(line) && !line.includes(OVERALL);
|
|
29624
|
+
}
|
|
29625
|
+
function suitesPrinted(printed) {
|
|
29626
|
+
const finished = [];
|
|
29627
|
+
let running = null;
|
|
29628
|
+
let failed = [];
|
|
29629
|
+
for (const line of printed) {
|
|
29630
|
+
const started = RUNNING_SUITE.exec(line);
|
|
29631
|
+
if (started?.[1] !== undefined) {
|
|
29632
|
+
running = started[1];
|
|
29633
|
+
failed = [];
|
|
29634
|
+
continue;
|
|
29635
|
+
}
|
|
29636
|
+
if (running === null) {
|
|
29637
|
+
continue;
|
|
29638
|
+
}
|
|
29639
|
+
const failing = CASE_FAILED.exec(line)?.[1];
|
|
29640
|
+
if (failing !== undefined) {
|
|
29641
|
+
failed.push(failing);
|
|
29642
|
+
continue;
|
|
29643
|
+
}
|
|
29644
|
+
const counted = finishesASuite(line) ? SUITE_COUNTS.exec(line) : null;
|
|
29645
|
+
if (counted !== null) {
|
|
29646
|
+
finished.push({
|
|
29647
|
+
path: running,
|
|
29648
|
+
cases: Number(counted[1]),
|
|
29649
|
+
errors: Number(counted[2]),
|
|
29650
|
+
failures: Number(counted[3]),
|
|
29651
|
+
failed
|
|
29652
|
+
});
|
|
29653
|
+
running = null;
|
|
29654
|
+
}
|
|
29655
|
+
}
|
|
29656
|
+
return { finished, running };
|
|
29657
|
+
}
|
|
29658
|
+
function testProgressMessage(printed, elapsedMs) {
|
|
29659
|
+
const { finished, running } = suitesPrinted(printed);
|
|
29660
|
+
const failing = finished.filter((suite) => suite.errors + suite.failures > 0).length;
|
|
29661
|
+
const seconds = Math.floor(elapsedMs / 1000);
|
|
29662
|
+
const took = seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
29663
|
+
const done = finished.length === 0 ? "No suite has finished" : `${finished.length === 1 ? "One suite has" : `${finished.length} suites have`} finished${failing === 0 ? "" : `, ${failing} with failures`}`;
|
|
29664
|
+
return `${done}${running === null ? "" : `; running ${running}`}; ${took} in.`;
|
|
29665
|
+
}
|
|
29576
29666
|
|
|
29577
29667
|
// src/keeper-channel.ts
|
|
29578
29668
|
import { connect, createServer as createServer2 } from "node:net";
|
|
@@ -29616,10 +29706,10 @@ function askTheKeeperToStop(address, withinMs) {
|
|
|
29616
29706
|
}
|
|
29617
29707
|
|
|
29618
29708
|
// src/lsp_client.ts
|
|
29619
|
-
import { existsSync as
|
|
29709
|
+
import { existsSync as existsSync12, realpathSync as realpathSync3 } from "node:fs";
|
|
29620
29710
|
import { readFile, realpath } from "node:fs/promises";
|
|
29621
29711
|
import { createConnection as createConnection3 } from "node:net";
|
|
29622
|
-
import { dirname as
|
|
29712
|
+
import { dirname as dirname9, join as join15, resolve as resolve3 } from "node:path";
|
|
29623
29713
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
29624
29714
|
var DEFAULT_LSP_PORT = 6005;
|
|
29625
29715
|
var DIAGNOSTICS_TIMEOUT_MS = 5000;
|
|
@@ -29776,7 +29866,7 @@ class GodotLSPClient {
|
|
|
29776
29866
|
if (this.initialized) {
|
|
29777
29867
|
return;
|
|
29778
29868
|
}
|
|
29779
|
-
const rootPath = this.rootPath ??
|
|
29869
|
+
const rootPath = this.rootPath ?? dirname9(resolve3(filePath));
|
|
29780
29870
|
await this.initialize(rootPath);
|
|
29781
29871
|
}
|
|
29782
29872
|
async sendRequest(method, params, timeoutMs = this.timeouts.requestMs) {
|
|
@@ -30187,7 +30277,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
|
|
|
30187
30277
|
} catch {
|
|
30188
30278
|
throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
|
|
30189
30279
|
}
|
|
30190
|
-
if (!
|
|
30280
|
+
if (!existsSync12(join15(projectPath, "project.godot"))) {
|
|
30191
30281
|
throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
|
|
30192
30282
|
}
|
|
30193
30283
|
const contained = resolveWithinProject(projectPath, scriptPathValue);
|
|
@@ -30364,14 +30454,14 @@ function secondsFromClock(printed) {
|
|
|
30364
30454
|
}
|
|
30365
30455
|
|
|
30366
30456
|
// src/project-scan.ts
|
|
30367
|
-
import { existsSync as
|
|
30368
|
-
import { join as
|
|
30457
|
+
import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync14 } from "node:fs";
|
|
30458
|
+
import { join as join16 } from "node:path";
|
|
30369
30459
|
var SKIPPED = new Set(["node_modules"]);
|
|
30370
30460
|
function skipped(name) {
|
|
30371
30461
|
return name.startsWith(".") || SKIPPED.has(name);
|
|
30372
30462
|
}
|
|
30373
30463
|
function steppedOver(directory) {
|
|
30374
|
-
return
|
|
30464
|
+
return existsSync13(join16(directory, ".gdignore"));
|
|
30375
30465
|
}
|
|
30376
30466
|
var SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
|
|
30377
30467
|
var ASSET_EXTENSIONS = new Set([
|
|
@@ -30415,7 +30505,7 @@ function projectStructure(projectPath) {
|
|
|
30415
30505
|
continue;
|
|
30416
30506
|
}
|
|
30417
30507
|
if (entry.isDirectory()) {
|
|
30418
|
-
visit(
|
|
30508
|
+
visit(join16(directory, entry.name));
|
|
30419
30509
|
} else if (entry.isFile()) {
|
|
30420
30510
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
30421
30511
|
if (SCENE_EXTENSIONS.has(extension)) {
|
|
@@ -30446,14 +30536,14 @@ function scriptsWithoutUid(projectPath) {
|
|
|
30446
30536
|
}
|
|
30447
30537
|
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
30448
30538
|
if (entry.isDirectory()) {
|
|
30449
|
-
visit(
|
|
30539
|
+
visit(join16(directory, entry.name), spelled);
|
|
30450
30540
|
continue;
|
|
30451
30541
|
}
|
|
30452
30542
|
if (!entry.isFile()) {
|
|
30453
30543
|
continue;
|
|
30454
30544
|
}
|
|
30455
30545
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
30456
|
-
if (CARRIES_A_UID.has(extension) && !
|
|
30546
|
+
if (CARRIES_A_UID.has(extension) && !existsSync13(join16(directory, `${entry.name}.uid`))) {
|
|
30457
30547
|
found.push(spelled);
|
|
30458
30548
|
}
|
|
30459
30549
|
}
|
|
@@ -30485,7 +30575,7 @@ function projectTexts(projectPath) {
|
|
|
30485
30575
|
if (skipped(entry.name)) {
|
|
30486
30576
|
continue;
|
|
30487
30577
|
}
|
|
30488
|
-
const absolute =
|
|
30578
|
+
const absolute = join16(directory, entry.name);
|
|
30489
30579
|
const spelled = `${prefix}${entry.name}`;
|
|
30490
30580
|
if (entry.isDirectory()) {
|
|
30491
30581
|
visit(absolute, `${spelled}/`);
|
|
@@ -30495,7 +30585,7 @@ function projectTexts(projectPath) {
|
|
|
30495
30585
|
if (!entry.isFile() || BINARY_EXTENSIONS.has(extension)) {
|
|
30496
30586
|
continue;
|
|
30497
30587
|
}
|
|
30498
|
-
const bytes =
|
|
30588
|
+
const bytes = readFileSync14(absolute);
|
|
30499
30589
|
if (!isBinary(bytes)) {
|
|
30500
30590
|
found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
|
|
30501
30591
|
}
|
|
@@ -30512,7 +30602,7 @@ function ignoredTexts(projectPath) {
|
|
|
30512
30602
|
if (skipped(entry.name)) {
|
|
30513
30603
|
continue;
|
|
30514
30604
|
}
|
|
30515
|
-
const absolute =
|
|
30605
|
+
const absolute = join16(directory, entry.name);
|
|
30516
30606
|
const spelled = `${prefix}${entry.name}`;
|
|
30517
30607
|
if (entry.isDirectory()) {
|
|
30518
30608
|
visit(absolute, `${spelled}/`, under);
|
|
@@ -30522,7 +30612,7 @@ function ignoredTexts(projectPath) {
|
|
|
30522
30612
|
if (!under || !entry.isFile() || BINARY_EXTENSIONS.has(extension) || entry.name === ".gdignore") {
|
|
30523
30613
|
continue;
|
|
30524
30614
|
}
|
|
30525
|
-
const bytes =
|
|
30615
|
+
const bytes = readFileSync14(absolute);
|
|
30526
30616
|
if (!isBinary(bytes)) {
|
|
30527
30617
|
found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
|
|
30528
30618
|
}
|
|
@@ -30552,7 +30642,7 @@ function searchProject(projectPath, options) {
|
|
|
30552
30642
|
if (skipped(entry.name)) {
|
|
30553
30643
|
continue;
|
|
30554
30644
|
}
|
|
30555
|
-
const entryPath =
|
|
30645
|
+
const entryPath = join16(directory, entry.name);
|
|
30556
30646
|
if (entry.isDirectory()) {
|
|
30557
30647
|
visit(entryPath);
|
|
30558
30648
|
continue;
|
|
@@ -30561,7 +30651,7 @@ function searchProject(projectPath, options) {
|
|
|
30561
30651
|
if (!entry.isFile() || extensions !== null && !extensions.has(extension)) {
|
|
30562
30652
|
continue;
|
|
30563
30653
|
}
|
|
30564
|
-
const bytes =
|
|
30654
|
+
const bytes = readFileSync14(entryPath);
|
|
30565
30655
|
if (extensions === null && isBinary(bytes)) {
|
|
30566
30656
|
continue;
|
|
30567
30657
|
}
|
|
@@ -30591,8 +30681,8 @@ function searchProject(projectPath, options) {
|
|
|
30591
30681
|
}
|
|
30592
30682
|
|
|
30593
30683
|
// src/rename.ts
|
|
30594
|
-
import { existsSync as
|
|
30595
|
-
import { dirname as
|
|
30684
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync8, readFileSync as readFileSync15, renameSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "node:fs";
|
|
30685
|
+
import { dirname as dirname10, posix } from "node:path";
|
|
30596
30686
|
class Script {
|
|
30597
30687
|
path;
|
|
30598
30688
|
text;
|
|
@@ -31314,25 +31404,25 @@ function rewritesOf(plan, texts, move) {
|
|
|
31314
31404
|
}
|
|
31315
31405
|
function applyRewrites(rewrites) {
|
|
31316
31406
|
for (const rewrite of rewrites) {
|
|
31317
|
-
const now =
|
|
31407
|
+
const now = readFileSync15(rewrite.absolute, "utf8");
|
|
31318
31408
|
if (now !== rewrite.before) {
|
|
31319
31409
|
throw new Error(`${rewrite.path} changed on disk while the rename was being worked out; nothing was written`);
|
|
31320
31410
|
}
|
|
31321
|
-
if (rewrite.writeTo !== rewrite.absolute &&
|
|
31411
|
+
if (rewrite.writeTo !== rewrite.absolute && existsSync14(rewrite.writeTo)) {
|
|
31322
31412
|
throw new Error(`${rewrite.writeTo} already exists; nothing was written`);
|
|
31323
31413
|
}
|
|
31324
31414
|
}
|
|
31325
31415
|
const done = [];
|
|
31326
31416
|
try {
|
|
31327
31417
|
for (const rewrite of rewrites) {
|
|
31328
|
-
|
|
31418
|
+
mkdirSync8(dirname10(rewrite.writeTo), { recursive: true });
|
|
31329
31419
|
const staging = `${rewrite.writeTo}.gdharness-rename`;
|
|
31330
|
-
|
|
31420
|
+
writeFileSync10(staging, rewrite.after, "utf8");
|
|
31331
31421
|
renameSync(staging, rewrite.writeTo);
|
|
31332
31422
|
let movedUid = false;
|
|
31333
31423
|
if (rewrite.writeTo !== rewrite.absolute) {
|
|
31334
31424
|
unlinkSync2(rewrite.absolute);
|
|
31335
|
-
if (
|
|
31425
|
+
if (existsSync14(`${rewrite.absolute}.uid`)) {
|
|
31336
31426
|
renameSync(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
|
|
31337
31427
|
movedUid = true;
|
|
31338
31428
|
}
|
|
@@ -31342,7 +31432,7 @@ function applyRewrites(rewrites) {
|
|
|
31342
31432
|
} catch (error) {
|
|
31343
31433
|
for (const { rewrite, movedUid } of done.reverse()) {
|
|
31344
31434
|
try {
|
|
31345
|
-
|
|
31435
|
+
writeFileSync10(rewrite.absolute, rewrite.before, "utf8");
|
|
31346
31436
|
if (rewrite.writeTo !== rewrite.absolute) {
|
|
31347
31437
|
unlinkSync2(rewrite.writeTo);
|
|
31348
31438
|
if (movedUid) {
|
|
@@ -31356,11 +31446,11 @@ function applyRewrites(rewrites) {
|
|
|
31356
31446
|
}
|
|
31357
31447
|
|
|
31358
31448
|
// src/rename-symbol.ts
|
|
31359
|
-
import { existsSync as
|
|
31360
|
-
import { basename as basename3, dirname as
|
|
31449
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync9, readdirSync as readdirSync6, realpathSync as realpathSync5, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "node:fs";
|
|
31450
|
+
import { basename as basename3, dirname as dirname11, join as join17, relative as relative3, resolve as resolve5 } from "node:path";
|
|
31361
31451
|
|
|
31362
31452
|
// src/resources.ts
|
|
31363
|
-
import { readFileSync as
|
|
31453
|
+
import { readFileSync as readFileSync16, realpathSync as realpathSync4 } from "node:fs";
|
|
31364
31454
|
import { extname, resolve as resolve4 } from "node:path";
|
|
31365
31455
|
var STATIC_RESOURCES = [
|
|
31366
31456
|
{
|
|
@@ -31570,7 +31660,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
31570
31660
|
const parsedUri = parseGodotUri(uri);
|
|
31571
31661
|
if (parsedUri.kind === "project-info") {
|
|
31572
31662
|
const projectFilePath = resolveProjectFile(projectPath, "project.godot");
|
|
31573
|
-
const rawProject =
|
|
31663
|
+
const rawProject = readFileSync16(projectFilePath, "utf-8");
|
|
31574
31664
|
const parsedProject = parseProjectGodot(rawProject);
|
|
31575
31665
|
return {
|
|
31576
31666
|
mimeType: "application/json",
|
|
@@ -31579,7 +31669,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
31579
31669
|
}
|
|
31580
31670
|
const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
|
|
31581
31671
|
ensureAllowedExtension(parsedUri.kind, filePath);
|
|
31582
|
-
const text =
|
|
31672
|
+
const text = readFileSync16(filePath, "utf-8");
|
|
31583
31673
|
return {
|
|
31584
31674
|
mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
|
|
31585
31675
|
text
|
|
@@ -31637,9 +31727,9 @@ function onDiskSpelling(path) {
|
|
|
31637
31727
|
let at = resolve5(path);
|
|
31638
31728
|
for (;; ) {
|
|
31639
31729
|
try {
|
|
31640
|
-
return
|
|
31730
|
+
return join17(realpathSync5.native(at), ...missing.reverse());
|
|
31641
31731
|
} catch {
|
|
31642
|
-
const parent =
|
|
31732
|
+
const parent = dirname11(at);
|
|
31643
31733
|
if (parent === at) {
|
|
31644
31734
|
return resolve5(path);
|
|
31645
31735
|
}
|
|
@@ -31690,7 +31780,7 @@ async function renameSymbol(request, services) {
|
|
|
31690
31780
|
reason: `newScriptPath must be a .gd path inside the project, and ${request.newScriptPath} is not.`
|
|
31691
31781
|
};
|
|
31692
31782
|
}
|
|
31693
|
-
if (
|
|
31783
|
+
if (existsSync15(request.newScriptPath)) {
|
|
31694
31784
|
return { ok: false, reason: `${moveTo} already exists, so the script cannot be moved there.` };
|
|
31695
31785
|
}
|
|
31696
31786
|
}
|
|
@@ -31866,7 +31956,7 @@ async function renameSymbol(request, services) {
|
|
|
31866
31956
|
scripts,
|
|
31867
31957
|
from: declaringPath,
|
|
31868
31958
|
to: moveTo,
|
|
31869
|
-
exists: (path) =>
|
|
31959
|
+
exists: (path) => existsSync15(`${projectPath}/${path.slice("res://".length)}`)
|
|
31870
31960
|
}, plan);
|
|
31871
31961
|
}
|
|
31872
31962
|
plan = mentionIgnored(plan, ignoredTexts(projectPath), symbol, moveTo === null ? null : declaringPath);
|
|
@@ -32067,15 +32157,15 @@ ${mention.kind}`;
|
|
|
32067
32157
|
var LEFT_ALONE_ANSWER_LINES = 300;
|
|
32068
32158
|
var detailWrites = 0;
|
|
32069
32159
|
function writeLeftAloneDetail(projectPath, symbol, detail) {
|
|
32070
|
-
const directory =
|
|
32071
|
-
|
|
32160
|
+
const directory = join17(projectPath, ".godot", "gdharness-renames");
|
|
32161
|
+
mkdirSync9(directory, { recursive: true });
|
|
32072
32162
|
detailWrites += 1;
|
|
32073
|
-
const path =
|
|
32074
|
-
|
|
32163
|
+
const path = join17(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
|
|
32164
|
+
writeFileSync11(path, `${JSON.stringify(detail, null, 2)}
|
|
32075
32165
|
`, "utf8");
|
|
32076
32166
|
const kept = readdirSync6(directory).filter((name) => name.endsWith(".json")).sort().reverse();
|
|
32077
32167
|
for (const old of kept.slice(10)) {
|
|
32078
|
-
|
|
32168
|
+
rmSync7(join17(directory, old), { force: true });
|
|
32079
32169
|
}
|
|
32080
32170
|
return path;
|
|
32081
32171
|
}
|
|
@@ -32098,16 +32188,16 @@ function summaryOf(edits, mentions, rewrites, left) {
|
|
|
32098
32188
|
}
|
|
32099
32189
|
|
|
32100
32190
|
// src/restart-note.ts
|
|
32101
|
-
import { mkdirSync as
|
|
32102
|
-
import { dirname as
|
|
32191
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync17, rmSync as rmSync8, writeFileSync as writeFileSync12 } from "node:fs";
|
|
32192
|
+
import { dirname as dirname12, join as join18 } from "node:path";
|
|
32103
32193
|
function restartNotePath(projectPath) {
|
|
32104
|
-
return
|
|
32194
|
+
return join18(projectPath, ".godot", "gdharness-restart.json");
|
|
32105
32195
|
}
|
|
32106
32196
|
function noteRestartBegun(note) {
|
|
32107
32197
|
const path = restartNotePath(note.projectPath);
|
|
32108
32198
|
try {
|
|
32109
|
-
|
|
32110
|
-
|
|
32199
|
+
mkdirSync10(dirname12(path), { recursive: true });
|
|
32200
|
+
writeFileSync12(path, `${JSON.stringify(note, null, 2)}
|
|
32111
32201
|
`, "utf8");
|
|
32112
32202
|
} catch (error) {
|
|
32113
32203
|
console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
|
|
@@ -32115,7 +32205,7 @@ function noteRestartBegun(note) {
|
|
|
32115
32205
|
}
|
|
32116
32206
|
function restartOwed(projectPath) {
|
|
32117
32207
|
try {
|
|
32118
|
-
const parsed = JSON.parse(
|
|
32208
|
+
const parsed = JSON.parse(readFileSync17(restartNotePath(projectPath), "utf8"));
|
|
32119
32209
|
if (typeof parsed !== "object" || parsed === null) {
|
|
32120
32210
|
return null;
|
|
32121
32211
|
}
|
|
@@ -32130,7 +32220,7 @@ function restartOwed(projectPath) {
|
|
|
32130
32220
|
}
|
|
32131
32221
|
function restartSettled(projectPath) {
|
|
32132
32222
|
try {
|
|
32133
|
-
|
|
32223
|
+
rmSync8(restartNotePath(projectPath), { force: true });
|
|
32134
32224
|
} catch (error) {
|
|
32135
32225
|
console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
|
|
32136
32226
|
}
|
|
@@ -32140,29 +32230,29 @@ function restartSettled(projectPath) {
|
|
|
32140
32230
|
import { execFile as execFile4 } from "node:child_process";
|
|
32141
32231
|
import { createHash as createHash3 } from "node:crypto";
|
|
32142
32232
|
import {
|
|
32143
|
-
mkdirSync as
|
|
32233
|
+
mkdirSync as mkdirSync11,
|
|
32144
32234
|
openSync as openSync2,
|
|
32145
32235
|
readdirSync as readdirSync7,
|
|
32146
|
-
readFileSync as
|
|
32236
|
+
readFileSync as readFileSync18,
|
|
32147
32237
|
realpathSync as realpathSync6,
|
|
32148
|
-
rmSync as
|
|
32238
|
+
rmSync as rmSync9,
|
|
32149
32239
|
statSync as statSync6,
|
|
32150
|
-
writeFileSync as
|
|
32240
|
+
writeFileSync as writeFileSync13
|
|
32151
32241
|
} from "node:fs";
|
|
32152
|
-
import { basename as basename4, dirname as
|
|
32242
|
+
import { basename as basename4, dirname as dirname13, join as join19, resolve as resolve6 } from "node:path";
|
|
32153
32243
|
import { promisify as promisify4 } from "node:util";
|
|
32154
32244
|
var execFileAsync = promisify4(execFile4);
|
|
32155
32245
|
function runsDirectory() {
|
|
32156
|
-
return
|
|
32246
|
+
return join19(runtimeDirectory(), "runs");
|
|
32157
32247
|
}
|
|
32158
32248
|
function realAsFarAsItExists(path) {
|
|
32159
32249
|
const missing = [];
|
|
32160
32250
|
let at = path;
|
|
32161
32251
|
for (;; ) {
|
|
32162
32252
|
try {
|
|
32163
|
-
return
|
|
32253
|
+
return join19(realpathSync6.native(at), ...missing);
|
|
32164
32254
|
} catch {
|
|
32165
|
-
const parent =
|
|
32255
|
+
const parent = dirname13(at);
|
|
32166
32256
|
if (parent === at) {
|
|
32167
32257
|
return path;
|
|
32168
32258
|
}
|
|
@@ -32177,27 +32267,27 @@ function projectKey(projectPath) {
|
|
|
32177
32267
|
return createHash3("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
32178
32268
|
}
|
|
32179
32269
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
32180
|
-
return
|
|
32270
|
+
return join19(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
32181
32271
|
}
|
|
32182
32272
|
var SHARED_NOTE = "run.json";
|
|
32183
32273
|
function openTranscript(startedAt) {
|
|
32184
|
-
|
|
32185
|
-
const path =
|
|
32274
|
+
mkdirSync11(runsDirectory(), { recursive: true });
|
|
32275
|
+
const path = join19(runsDirectory(), `run-${startedAt}.log`);
|
|
32186
32276
|
return { path, fd: openSync2(path, "a") };
|
|
32187
32277
|
}
|
|
32188
32278
|
function noteIsAbout(note, playing) {
|
|
32189
32279
|
return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
|
|
32190
32280
|
}
|
|
32191
32281
|
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
32192
|
-
return
|
|
32282
|
+
return join19(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
32193
32283
|
}
|
|
32194
32284
|
function writeEditorRunNote(note) {
|
|
32195
|
-
|
|
32196
|
-
|
|
32285
|
+
mkdirSync11(runsDirectory(), { recursive: true });
|
|
32286
|
+
writeFileSync13(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
|
|
32197
32287
|
}
|
|
32198
32288
|
function readEditorRunNote(projectPath) {
|
|
32199
32289
|
for (const directory of runtimeDirectories()) {
|
|
32200
|
-
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(
|
|
32290
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join19(directory, "runs", "editor-run.json"));
|
|
32201
32291
|
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
32202
32292
|
return note;
|
|
32203
32293
|
}
|
|
@@ -32207,7 +32297,7 @@ function readEditorRunNote(projectPath) {
|
|
|
32207
32297
|
function editorNoteAt(path) {
|
|
32208
32298
|
let parsed;
|
|
32209
32299
|
try {
|
|
32210
|
-
parsed = JSON.parse(
|
|
32300
|
+
parsed = JSON.parse(readFileSync18(path, "utf8"));
|
|
32211
32301
|
} catch {
|
|
32212
32302
|
return null;
|
|
32213
32303
|
}
|
|
@@ -32231,7 +32321,7 @@ function readRunRecord(projectPath) {
|
|
|
32231
32321
|
if (own !== null) {
|
|
32232
32322
|
return own;
|
|
32233
32323
|
}
|
|
32234
|
-
const shared = recordAt(
|
|
32324
|
+
const shared = recordAt(join19(directory, "runs", SHARED_NOTE));
|
|
32235
32325
|
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
32236
32326
|
return shared;
|
|
32237
32327
|
}
|
|
@@ -32243,13 +32333,13 @@ function everyRunRecord() {
|
|
|
32243
32333
|
for (const directory of runtimeDirectories()) {
|
|
32244
32334
|
let entries;
|
|
32245
32335
|
try {
|
|
32246
|
-
entries = readdirSync7(
|
|
32336
|
+
entries = readdirSync7(join19(directory, "runs"));
|
|
32247
32337
|
} catch {
|
|
32248
32338
|
continue;
|
|
32249
32339
|
}
|
|
32250
32340
|
for (const entry of entries) {
|
|
32251
32341
|
if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
32252
|
-
const record = recordAt(
|
|
32342
|
+
const record = recordAt(join19(directory, "runs", entry));
|
|
32253
32343
|
if (record !== null) {
|
|
32254
32344
|
found.push(record);
|
|
32255
32345
|
}
|
|
@@ -32261,7 +32351,7 @@ function everyRunRecord() {
|
|
|
32261
32351
|
function recordAt(path) {
|
|
32262
32352
|
let parsed;
|
|
32263
32353
|
try {
|
|
32264
|
-
parsed = JSON.parse(
|
|
32354
|
+
parsed = JSON.parse(readFileSync18(path, "utf8"));
|
|
32265
32355
|
} catch {
|
|
32266
32356
|
return null;
|
|
32267
32357
|
}
|
|
@@ -32312,7 +32402,7 @@ async function runningAs(pid) {
|
|
|
32312
32402
|
const began = await startedAt(pid, null);
|
|
32313
32403
|
const when = began === null ? {} : { startedAt: began };
|
|
32314
32404
|
if (process.platform === "linux") {
|
|
32315
|
-
const raw =
|
|
32405
|
+
const raw = readFileSync18(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
32316
32406
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
32317
32407
|
}
|
|
32318
32408
|
const args = (await execFileAsync("ps", ["-p", String(pid), "-o", "args="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
|
|
@@ -32458,12 +32548,12 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
|
32458
32548
|
}
|
|
32459
32549
|
function clearRunRecord(projectPath) {
|
|
32460
32550
|
for (const directory of runtimeDirectories()) {
|
|
32461
|
-
const shared =
|
|
32551
|
+
const shared = join19(directory, "runs", SHARED_NOTE);
|
|
32462
32552
|
const sharedRecord = recordAt(shared);
|
|
32463
32553
|
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
32464
32554
|
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
32465
32555
|
try {
|
|
32466
|
-
|
|
32556
|
+
rmSync9(path, { force: true });
|
|
32467
32557
|
} catch {}
|
|
32468
32558
|
}
|
|
32469
32559
|
}
|
|
@@ -32488,31 +32578,124 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
32488
32578
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
32489
32579
|
continue;
|
|
32490
32580
|
}
|
|
32491
|
-
const path =
|
|
32581
|
+
const path = join19(directory, entry);
|
|
32492
32582
|
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
32493
32583
|
continue;
|
|
32494
32584
|
}
|
|
32495
32585
|
try {
|
|
32496
|
-
|
|
32586
|
+
rmSync9(path);
|
|
32497
32587
|
} catch {}
|
|
32498
32588
|
}
|
|
32499
32589
|
}
|
|
32500
32590
|
|
|
32591
|
+
// src/script-origins.ts
|
|
32592
|
+
import { join as join20 } from "node:path";
|
|
32593
|
+
var UNRESOLVED = /Could not resolve class "([A-Za-z_][A-Za-z0-9_]*)", because of a parser error/;
|
|
32594
|
+
function unresolvedClassIn(message) {
|
|
32595
|
+
return UNRESOLVED.exec(message)?.[1] ?? null;
|
|
32596
|
+
}
|
|
32597
|
+
var MOST_CHECKED = 8;
|
|
32598
|
+
async function originsOf(errors, declared, check) {
|
|
32599
|
+
const listed = new Set(errors.map((error) => error.path));
|
|
32600
|
+
const origins = [];
|
|
32601
|
+
const unconfirmed = [];
|
|
32602
|
+
const asked = new Set;
|
|
32603
|
+
const waiting = errors.map((error) => unresolvedClassIn(error.message)).filter((name) => name !== null);
|
|
32604
|
+
while (waiting.length > 0 && asked.size < MOST_CHECKED) {
|
|
32605
|
+
const name = waiting.shift() ?? "";
|
|
32606
|
+
const path = declared.get(name);
|
|
32607
|
+
if (path === undefined || listed.has(path) || asked.has(path)) {
|
|
32608
|
+
continue;
|
|
32609
|
+
}
|
|
32610
|
+
asked.add(path);
|
|
32611
|
+
const checked = await check(path);
|
|
32612
|
+
const own = checked.errors.filter((error) => error.path === path);
|
|
32613
|
+
if (own.length === 0) {
|
|
32614
|
+
unconfirmed.push({
|
|
32615
|
+
path,
|
|
32616
|
+
declares: name,
|
|
32617
|
+
why: checked.failure === null ? "the engine's check of it found no error" : `the engine's check of it did not finish (${checked.failure})`
|
|
32618
|
+
});
|
|
32619
|
+
continue;
|
|
32620
|
+
}
|
|
32621
|
+
for (const error of own) {
|
|
32622
|
+
origins.push({ ...error, declares: name });
|
|
32623
|
+
const deeper = unresolvedClassIn(error.message);
|
|
32624
|
+
if (deeper !== null) {
|
|
32625
|
+
waiting.push(deeper);
|
|
32626
|
+
}
|
|
32627
|
+
}
|
|
32628
|
+
}
|
|
32629
|
+
return { origins, unconfirmed };
|
|
32630
|
+
}
|
|
32631
|
+
var RELOADED_AT = /GDScript::reload \((res:\/\/.+):(\d+)\)$/;
|
|
32632
|
+
function reloadedAt(entry) {
|
|
32633
|
+
for (const line of entry.detail) {
|
|
32634
|
+
const at = RELOADED_AT.exec(line.trim());
|
|
32635
|
+
if (at !== null) {
|
|
32636
|
+
return { path: at[1] ?? "", line: Number(at[2]) === 0 ? null : Number(at[2]) };
|
|
32637
|
+
}
|
|
32638
|
+
}
|
|
32639
|
+
return null;
|
|
32640
|
+
}
|
|
32641
|
+
function checkWithEngine(godotPath, projectPath) {
|
|
32642
|
+
return async (path) => {
|
|
32643
|
+
const logDir = scratchDirectory("tests");
|
|
32644
|
+
try {
|
|
32645
|
+
const ran = await runEngine(godotPath, [
|
|
32646
|
+
"--headless",
|
|
32647
|
+
"--log-file",
|
|
32648
|
+
join20(logDir, "engine.log"),
|
|
32649
|
+
"--path",
|
|
32650
|
+
projectPath,
|
|
32651
|
+
"--check-only",
|
|
32652
|
+
"-s",
|
|
32653
|
+
path
|
|
32654
|
+
], { timeout: { ms: 60000, said: "minute" } });
|
|
32655
|
+
const errors = ran.log.select({ severity: "error", sinceLastCall: false, limit: 50 }).entries.flatMap((entry) => {
|
|
32656
|
+
const at = reloadedAt(entry);
|
|
32657
|
+
return at === null ? [] : [{ ...at, message: entry.text }];
|
|
32658
|
+
});
|
|
32659
|
+
return { errors, failure: ran.failure === null ? null : howItEnded(ran) };
|
|
32660
|
+
} finally {
|
|
32661
|
+
discard(logDir);
|
|
32662
|
+
}
|
|
32663
|
+
};
|
|
32664
|
+
}
|
|
32665
|
+
function withoutListed(entries, errors) {
|
|
32666
|
+
if (errors.length === 0) {
|
|
32667
|
+
return [...entries];
|
|
32668
|
+
}
|
|
32669
|
+
const listing = discoveryListing(entries.map((entry) => entry.text));
|
|
32670
|
+
const paths = new Set(errors.map((error) => error.path));
|
|
32671
|
+
return entries.filter((entry, at) => {
|
|
32672
|
+
if (listing !== null && at >= listing.from && at < listing.to) {
|
|
32673
|
+
return false;
|
|
32674
|
+
}
|
|
32675
|
+
const reloaded = reloadedAt(entry);
|
|
32676
|
+
if (reloaded !== null && errors.some((error) => error.path === reloaded.path && error.line === reloaded.line && error.message === entry.text)) {
|
|
32677
|
+
return false;
|
|
32678
|
+
}
|
|
32679
|
+
const failedToLoad = /^Failed to load script "(res:\/\/[^"]+)" with error "[^"]+"\.$/.exec(entry.text);
|
|
32680
|
+
return failedToLoad === null || !paths.has(failedToLoad[1] ?? "");
|
|
32681
|
+
});
|
|
32682
|
+
}
|
|
32683
|
+
|
|
32501
32684
|
// src/setup.ts
|
|
32502
32685
|
import { spawnSync } from "node:child_process";
|
|
32503
32686
|
import { createHash as createHash4 } from "node:crypto";
|
|
32504
32687
|
import {
|
|
32505
32688
|
cpSync,
|
|
32506
|
-
existsSync as
|
|
32507
|
-
mkdirSync as
|
|
32689
|
+
existsSync as existsSync16,
|
|
32690
|
+
mkdirSync as mkdirSync12,
|
|
32508
32691
|
readdirSync as readdirSync8,
|
|
32509
|
-
readFileSync as
|
|
32692
|
+
readFileSync as readFileSync19,
|
|
32510
32693
|
rmdirSync,
|
|
32511
|
-
rmSync as
|
|
32694
|
+
rmSync as rmSync10,
|
|
32512
32695
|
statSync as statSync7,
|
|
32513
|
-
writeFileSync as
|
|
32696
|
+
writeFileSync as writeFileSync14
|
|
32514
32697
|
} from "node:fs";
|
|
32515
|
-
import { dirname as
|
|
32698
|
+
import { dirname as dirname14, join as join21 } from "node:path";
|
|
32516
32699
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
32517
32700
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
32518
32701
|
var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
|
|
@@ -32536,21 +32719,21 @@ function addonsDigest(names, from) {
|
|
|
32536
32719
|
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
32537
32720
|
continue;
|
|
32538
32721
|
}
|
|
32539
|
-
const path =
|
|
32722
|
+
const path = join21(directory, name);
|
|
32540
32723
|
const inside = `${relative}/${name}`;
|
|
32541
32724
|
if (statSync7(path).isDirectory()) {
|
|
32542
32725
|
walk(path, inside);
|
|
32543
32726
|
} else {
|
|
32544
32727
|
hash.update(`${inside}
|
|
32545
32728
|
`);
|
|
32546
|
-
hash.update(
|
|
32729
|
+
hash.update(readFileSync19(path));
|
|
32547
32730
|
hash.update(`
|
|
32548
32731
|
`);
|
|
32549
32732
|
}
|
|
32550
32733
|
}
|
|
32551
32734
|
};
|
|
32552
32735
|
for (const name of names) {
|
|
32553
|
-
walk(
|
|
32736
|
+
walk(join21(from, name), name);
|
|
32554
32737
|
}
|
|
32555
32738
|
return hash.digest("hex");
|
|
32556
32739
|
}
|
|
@@ -32583,22 +32766,22 @@ function installedRuntimeDigest(projectPath) {
|
|
|
32583
32766
|
return digestMarker(projectPath, RUNTIME_ADDON);
|
|
32584
32767
|
}
|
|
32585
32768
|
function digestMarker(projectPath, addon) {
|
|
32586
|
-
const marker =
|
|
32587
|
-
if (!
|
|
32769
|
+
const marker = join21(projectPath, "addons", addon, DIGEST_MARKER);
|
|
32770
|
+
if (!existsSync16(marker)) {
|
|
32588
32771
|
return null;
|
|
32589
32772
|
}
|
|
32590
|
-
const said =
|
|
32773
|
+
const said = readFileSync19(marker, "utf8").trim();
|
|
32591
32774
|
return said === "" ? null : said;
|
|
32592
32775
|
}
|
|
32593
32776
|
function shippedAddonsDirectory() {
|
|
32594
|
-
return
|
|
32777
|
+
return join21(dirname14(fileURLToPath3(import.meta.url)), "godot", "addons");
|
|
32595
32778
|
}
|
|
32596
32779
|
function installedAddonVersion(projectPath) {
|
|
32597
|
-
const marker =
|
|
32598
|
-
if (!
|
|
32780
|
+
const marker = join21(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
32781
|
+
if (!existsSync16(marker)) {
|
|
32599
32782
|
return null;
|
|
32600
32783
|
}
|
|
32601
|
-
const said =
|
|
32784
|
+
const said = readFileSync19(marker, "utf8").trim();
|
|
32602
32785
|
return said === "" ? null : said;
|
|
32603
32786
|
}
|
|
32604
32787
|
function loaderAmong(projectPath, named) {
|
|
@@ -32609,7 +32792,7 @@ function loaderAmong(projectPath, named) {
|
|
|
32609
32792
|
}
|
|
32610
32793
|
let source = null;
|
|
32611
32794
|
try {
|
|
32612
|
-
source =
|
|
32795
|
+
source = readFileSync19(join21(projectPath, path), "utf8");
|
|
32613
32796
|
} catch {}
|
|
32614
32797
|
if (source?.includes(RUNTIME_AUTOLOAD.path)) {
|
|
32615
32798
|
return { name: entry, path, exists: true };
|
|
@@ -32651,10 +32834,10 @@ function enabledPlugins(settings) {
|
|
|
32651
32834
|
function inspectProject(projectPath) {
|
|
32652
32835
|
const problems = [];
|
|
32653
32836
|
const addons = ADDONS.map((name) => {
|
|
32654
|
-
const directory =
|
|
32655
|
-
const marker =
|
|
32656
|
-
const installed =
|
|
32657
|
-
const version =
|
|
32837
|
+
const directory = join21(projectPath, "addons", name);
|
|
32838
|
+
const marker = join21(directory, VERSION_MARKER);
|
|
32839
|
+
const installed = existsSync16(directory);
|
|
32840
|
+
const version = existsSync16(marker) ? readFileSync19(marker, "utf8").trim() : null;
|
|
32658
32841
|
const current = version === SERVER_VERSION;
|
|
32659
32842
|
if (!installed) {
|
|
32660
32843
|
problems.push(`addons/${name} is not installed; run gdharness setup`);
|
|
@@ -32663,7 +32846,7 @@ function inspectProject(projectPath) {
|
|
|
32663
32846
|
}
|
|
32664
32847
|
return { name, installed, version, current };
|
|
32665
32848
|
});
|
|
32666
|
-
const settings = parseProjectGodot(
|
|
32849
|
+
const settings = parseProjectGodot(readFileSync19(join21(projectPath, "project.godot"), "utf8"));
|
|
32667
32850
|
const pluginsEnabled = enabledPlugins(settings);
|
|
32668
32851
|
for (const name of EDITOR_PLUGINS) {
|
|
32669
32852
|
if (!pluginsEnabled.includes(name)) {
|
|
@@ -32679,7 +32862,7 @@ function inspectProject(projectPath) {
|
|
|
32679
32862
|
const tracked = trackedByGit(projectPath, [...named.values()]);
|
|
32680
32863
|
const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
|
|
32681
32864
|
for (const [name, path] of named) {
|
|
32682
|
-
if (!
|
|
32865
|
+
if (!existsSync16(join21(projectPath, path))) {
|
|
32683
32866
|
if (!addonMissing(path)) {
|
|
32684
32867
|
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
32868
|
}
|
|
@@ -33085,7 +33268,7 @@ var TOOL_SPECS = [
|
|
|
33085
33268
|
},
|
|
33086
33269
|
{
|
|
33087
33270
|
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 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. 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.",
|
|
33271
|
+
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, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. 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. Needs gdUnit4 under addons/gdUnit4.",
|
|
33089
33272
|
parameters: {
|
|
33090
33273
|
projectPath: PROJECT_PATH,
|
|
33091
33274
|
path: {
|
|
@@ -33655,7 +33838,7 @@ var TOOL_SPECS = [
|
|
|
33655
33838
|
says: {
|
|
33656
33839
|
type: "string",
|
|
33657
33840
|
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.'
|
|
33841
|
+
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
33842
|
},
|
|
33660
33843
|
limit: {
|
|
33661
33844
|
type: "integer",
|
|
@@ -33833,7 +34016,7 @@ var TOOL_SPECS = [
|
|
|
33833
34016
|
says: {
|
|
33834
34017
|
type: "string",
|
|
33835
34018
|
ops: ["click"],
|
|
33836
|
-
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
|
|
34019
|
+
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."
|
|
33837
34020
|
},
|
|
33838
34021
|
action: {
|
|
33839
34022
|
type: "string",
|
|
@@ -33910,7 +34093,7 @@ var TOOL_SPECS = [
|
|
|
33910
34093
|
requires: [],
|
|
33911
34094
|
operations: {
|
|
33912
34095
|
click: {
|
|
33913
|
-
summary: `press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. Words that are part of a RichTextLabel are pressed where they are drawn rather than at the label's middle, so a link among them is the one pressed: found.pressedOn says "the words", or "the middle of the label" with a note saying why they could not be placed, and link_pressed
|
|
34096
|
+
summary: `press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. Words that are part of a RichTextLabel are pressed where they are drawn rather than at the label's middle, so a link among them is the one pressed: found.pressedOn says "the words", or "the middle of the label" with a note saying why they could not be placed, and 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 out of sight inside a ScrollContainer is scrolled to first, each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A disabled button, and a control that does not process (the game paused, say), are refused: the pointer reaches them and the press does nothing. A 3D node is clicked where it is drawn, and landed then 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`,
|
|
33914
34097
|
requires: []
|
|
33915
34098
|
},
|
|
33916
34099
|
choose: {
|
|
@@ -34089,7 +34272,7 @@ function buildToolDefinitions() {
|
|
|
34089
34272
|
var UPDATE_NOTICE_EVERY = 500;
|
|
34090
34273
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
34091
34274
|
var run4 = promisify5(execFile5);
|
|
34092
|
-
var __dirname2 =
|
|
34275
|
+
var __dirname2 = dirname15(fileURLToPath4(import.meta.url));
|
|
34093
34276
|
function editorRestartTimeoutMs() {
|
|
34094
34277
|
const override = Number.parseInt(envValue("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS") ?? "", 10);
|
|
34095
34278
|
return Number.isInteger(override) && override > 0 ? override : 90000;
|
|
@@ -34110,16 +34293,16 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
34110
34293
|
if (extname2(outputPath).toLowerCase() !== ".png") {
|
|
34111
34294
|
return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
|
|
34112
34295
|
}
|
|
34113
|
-
const directory = realPathOr(
|
|
34114
|
-
if (!
|
|
34115
|
-
return `outputPath's directory ${
|
|
34296
|
+
const directory = realPathOr(dirname15(outputPath));
|
|
34297
|
+
if (!existsSync17(directory)) {
|
|
34298
|
+
return `outputPath's directory ${dirname15(outputPath)} does not exist; a capture creates no directories.`;
|
|
34116
34299
|
}
|
|
34117
|
-
const where =
|
|
34300
|
+
const where = join22(directory, basename5(outputPath));
|
|
34118
34301
|
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
34119
34302
|
if (inside !== undefined) {
|
|
34120
34303
|
return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
|
|
34121
34304
|
}
|
|
34122
|
-
if (
|
|
34305
|
+
if (existsSync17(where)) {
|
|
34123
34306
|
return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
|
|
34124
34307
|
}
|
|
34125
34308
|
return null;
|
|
@@ -34623,20 +34806,75 @@ function settingsNamedBy(op, args) {
|
|
|
34623
34806
|
}
|
|
34624
34807
|
}
|
|
34625
34808
|
var STALLED_AFTER_MS = 30000;
|
|
34809
|
+
var TEST_HEARTBEAT_MS = 20000;
|
|
34626
34810
|
var DEPENDED_FAILED = "Failed to compile depended scripts";
|
|
34627
34811
|
function foldedScriptErrors(errors) {
|
|
34628
|
-
const
|
|
34812
|
+
const unresolved = new Map;
|
|
34813
|
+
for (const error of errors) {
|
|
34814
|
+
const name = unresolvedClassIn(error.message);
|
|
34815
|
+
if (name !== null) {
|
|
34816
|
+
unresolved.set(error.path, (unresolved.get(error.path) ?? new Set).add(name));
|
|
34817
|
+
}
|
|
34818
|
+
}
|
|
34819
|
+
const follows = (error) => {
|
|
34820
|
+
const names = unresolved.get(error.path);
|
|
34821
|
+
if (names === undefined) {
|
|
34822
|
+
return false;
|
|
34823
|
+
}
|
|
34824
|
+
const reparsed = /Could not parse global class "([A-Za-z_][A-Za-z0-9_]*)"/.exec(error.message)?.[1];
|
|
34825
|
+
return error.message.includes('the inferred type "Variant"') || reparsed !== undefined && names.has(reparsed);
|
|
34826
|
+
};
|
|
34827
|
+
const scriptErrors = errors.filter((error) => !error.message.includes(DEPENDED_FAILED) && !follows(error));
|
|
34629
34828
|
const own = new Set(scriptErrors.map((error) => error.path));
|
|
34630
34829
|
return {
|
|
34631
34830
|
scriptErrors,
|
|
34632
|
-
failedThroughDependency: [...new Set(errors.map((error) => error.path))].filter((path) => !own.has(path))
|
|
34831
|
+
failedThroughDependency: [...new Set(errors.map((error) => error.path))].filter((path) => !own.has(path)),
|
|
34832
|
+
followFromUnresolved: errors.filter(follows)
|
|
34633
34833
|
};
|
|
34634
34834
|
}
|
|
34635
34835
|
function nonEmpty(lists) {
|
|
34636
34836
|
return Object.fromEntries(Object.entries(lists).filter(([, list]) => list.length > 0));
|
|
34637
34837
|
}
|
|
34638
|
-
function
|
|
34639
|
-
const
|
|
34838
|
+
function withPreviousCut(answer, previous) {
|
|
34839
|
+
const content = answer.content.map((part) => ({ ...part }));
|
|
34840
|
+
const last = content.at(-1);
|
|
34841
|
+
let json = null;
|
|
34842
|
+
try {
|
|
34843
|
+
const parsed = JSON.parse(last?.text ?? "");
|
|
34844
|
+
json = typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? { ...parsed } : null;
|
|
34845
|
+
} catch {}
|
|
34846
|
+
if (last !== undefined && json !== null) {
|
|
34847
|
+
last.text = answerJson({ ...json, previousRunCut: previous });
|
|
34848
|
+
}
|
|
34849
|
+
const note = cutTestRunNote(previous);
|
|
34850
|
+
const first = content[0];
|
|
34851
|
+
if (first === undefined || first === last && json !== null || typeof first.text !== "string") {
|
|
34852
|
+
content.unshift({ type: "text", text: note });
|
|
34853
|
+
} else {
|
|
34854
|
+
first.text = `${first.text} ${note}`;
|
|
34855
|
+
}
|
|
34856
|
+
return { ...answer, content };
|
|
34857
|
+
}
|
|
34858
|
+
function finishedBeforeTheEnd(finished, failing, running) {
|
|
34859
|
+
if (finished === 0 && running === null) {
|
|
34860
|
+
return "";
|
|
34861
|
+
}
|
|
34862
|
+
const got = finished === 0 ? "no suite had finished" : `${finished === 1 ? "one suite" : `${finished} suites`} finished${failing === 0 ? "" : finished === 1 ? ", with failures" : `, ${failing} of them with failures`}`;
|
|
34863
|
+
const listed = finished === 0 ? "" : `; ${finished === 1 ? "it is" : "they are"} under suitesFinished${failing === 0 ? "" : ", with the failing cases under suitesFailing"}, so a run of the rest need not repeat ${finished === 1 ? "it" : "them"}`;
|
|
34864
|
+
return ` Before it ended, ${got}${running === null ? "" : `${finished === 0 ? " and" : ", and"} ${running} was running`}${listed}.`;
|
|
34865
|
+
}
|
|
34866
|
+
function scriptErrorsNote(errors, {
|
|
34867
|
+
origins,
|
|
34868
|
+
unconfirmed
|
|
34869
|
+
} = {
|
|
34870
|
+
origins: [],
|
|
34871
|
+
unconfirmed: []
|
|
34872
|
+
}) {
|
|
34873
|
+
const {
|
|
34874
|
+
scriptErrors: own,
|
|
34875
|
+
failedThroughDependency: dependents,
|
|
34876
|
+
followFromUnresolved: consequences
|
|
34877
|
+
} = foldedScriptErrors(errors);
|
|
34640
34878
|
const causes = new Map;
|
|
34641
34879
|
for (const error of own) {
|
|
34642
34880
|
if (!causes.has(error.path)) {
|
|
@@ -34644,16 +34882,29 @@ function scriptErrorsNote(errors) {
|
|
|
34644
34882
|
}
|
|
34645
34883
|
}
|
|
34646
34884
|
const scripts = causes.size + dependents.length;
|
|
34647
|
-
const
|
|
34885
|
+
const at = (error) => error.line === null ? error.path : `${error.path}:${error.line}`;
|
|
34886
|
+
const sentence = (text) => `${text}${/[.!?]\)?$/.test(text) ? "" : "."}`;
|
|
34887
|
+
const traced = new Set;
|
|
34888
|
+
const failedOn = (declares) => {
|
|
34889
|
+
const through = [...causes.values()].filter((error) => unresolvedClassIn(error.message) === declares);
|
|
34890
|
+
for (const error of through) {
|
|
34891
|
+
traced.add(error.path);
|
|
34892
|
+
}
|
|
34893
|
+
return through.length === 0 ? "" : ` ${through.map(at).join(", ")} could not resolve ${declares} because of it.`;
|
|
34894
|
+
};
|
|
34895
|
+
const first = [...new Map(origins.map((origin) => [origin.path, origin])).values()].sort((a, b) => Number(unresolvedClassIn(a.message) !== null) - Number(unresolvedClassIn(b.message) !== null));
|
|
34896
|
+
const named = [
|
|
34897
|
+
...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)}`),
|
|
34898
|
+
...unconfirmed.map((origin) => `${origin.path} declares ${origin.declares}, which gdUnit4 did not name, and ${origin.why}.${failedOn(origin.declares)}`),
|
|
34899
|
+
...[...causes.values()].filter((error) => !traced.has(error.path)).map((error) => `${at(error)} ${sentence(error.message)}`)
|
|
34900
|
+
].join(" ");
|
|
34648
34901
|
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(", ")}.`;
|
|
34649
34902
|
const more = own.length - causes.size;
|
|
34650
34903
|
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.` : "";
|
|
34651
|
-
|
|
34652
|
-
|
|
34653
|
-
|
|
34654
|
-
|
|
34655
|
-
rest
|
|
34656
|
-
].filter((part) => part !== "").join(" ");
|
|
34904
|
+
const after = consequences.length > 0 ? `${consequences.length === 1 ? "One more error comes" : `${consequences.length} more errors come`} only from a class that did not resolve, which the engine reads as Variant, and ${consequences.length === 1 ? "is" : "are"} under followFromUnresolved.` : "";
|
|
34905
|
+
const unnamed = first.length;
|
|
34906
|
+
const opening = unnamed === 0 ? `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.` : `No tests ran: ${scripts + unnamed} scripts did not compile, and gdUnit4 runs no suite at all when one fails to load. It could not load ${scripts === 1 ? "one" : scripts} of them while looking for suites, and ${unnamed === 1 ? "the one it did not name comes" : `the ${unnamed} it did not name come`} first.`;
|
|
34907
|
+
return [opening, named, following, rest, after].filter((part) => part !== "").join(" ");
|
|
34657
34908
|
}
|
|
34658
34909
|
function timedOutVerdict(timeoutMs, silentForMs) {
|
|
34659
34910
|
const quiet = Math.round(silentForMs);
|
|
@@ -34743,7 +34994,7 @@ function aboveTheRunner(entry) {
|
|
|
34743
34994
|
}
|
|
34744
34995
|
function elsewhereIn(projectPath, asked) {
|
|
34745
34996
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
34746
|
-
if (wanted === "" ||
|
|
34997
|
+
if (wanted === "" || existsSync17(join22(projectPath, wanted))) {
|
|
34747
34998
|
return "";
|
|
34748
34999
|
}
|
|
34749
35000
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -34787,7 +35038,7 @@ function testPathsIn(projectPath, given) {
|
|
|
34787
35038
|
};
|
|
34788
35039
|
}
|
|
34789
35040
|
function hasMainScene(projectFile) {
|
|
34790
|
-
const scene = parseProjectGodot(
|
|
35041
|
+
const scene = parseProjectGodot(readFileSync20(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
34791
35042
|
return typeof scene === "string" && scene !== "";
|
|
34792
35043
|
}
|
|
34793
35044
|
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
@@ -34795,13 +35046,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
34795
35046
|
return false;
|
|
34796
35047
|
}
|
|
34797
35048
|
try {
|
|
34798
|
-
return
|
|
35049
|
+
return readFileSync20(join22(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
34799
35050
|
} catch {
|
|
34800
35051
|
return false;
|
|
34801
35052
|
}
|
|
34802
35053
|
}
|
|
34803
35054
|
function editorPlaysHeadless(projectFile) {
|
|
34804
|
-
const runArgs = parseProjectGodot(
|
|
35055
|
+
const runArgs = parseProjectGodot(readFileSync20(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
34805
35056
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
34806
35057
|
}
|
|
34807
35058
|
function camelCased(params) {
|
|
@@ -34820,7 +35071,7 @@ function needsItsOwnProcess(asked) {
|
|
|
34820
35071
|
class GodotServer {
|
|
34821
35072
|
mcp;
|
|
34822
35073
|
locator = new GodotLocator;
|
|
34823
|
-
operationsScript =
|
|
35074
|
+
operationsScript = join22(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
34824
35075
|
godotBridge;
|
|
34825
35076
|
tools = buildToolDefinitions();
|
|
34826
35077
|
activeProcess = null;
|
|
@@ -35123,11 +35374,20 @@ class GodotServer {
|
|
|
35123
35374
|
return checked.response;
|
|
35124
35375
|
}
|
|
35125
35376
|
const named = args["projectPath"];
|
|
35126
|
-
if (typeof named === "string" &&
|
|
35377
|
+
if (typeof named === "string" && existsSync17(join22(named, "project.godot"))) {
|
|
35127
35378
|
this.lastProjectPath = named;
|
|
35128
35379
|
}
|
|
35129
35380
|
this.updates.refresh();
|
|
35130
|
-
const
|
|
35381
|
+
const token = request.params._meta?.progressToken;
|
|
35382
|
+
const progress = token === undefined ? undefined : (done, message) => {
|
|
35383
|
+
extra.sendNotification({
|
|
35384
|
+
method: "notifications/progress",
|
|
35385
|
+
params: { progressToken: token, progress: done, message }
|
|
35386
|
+
}).catch((error) => {
|
|
35387
|
+
this.logDebug(`Progress for ${spec.name} was not sent: ${errorMessage(error)}`);
|
|
35388
|
+
});
|
|
35389
|
+
};
|
|
35390
|
+
const answer = await withCallSignal(extra.signal, () => this.answered(spec.name, checked.op ?? "", args), progress);
|
|
35131
35391
|
return this.withFeedbackNotice(this.withUpdateNotice(answer));
|
|
35132
35392
|
});
|
|
35133
35393
|
}
|
|
@@ -35597,8 +35857,8 @@ class GodotServer {
|
|
|
35597
35857
|
if (path === undefined) {
|
|
35598
35858
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
35599
35859
|
}
|
|
35600
|
-
const file =
|
|
35601
|
-
if (!
|
|
35860
|
+
const file = join22(path, "project.godot");
|
|
35861
|
+
if (!existsSync17(file)) {
|
|
35602
35862
|
return {
|
|
35603
35863
|
ok: false,
|
|
35604
35864
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -36209,7 +36469,7 @@ class GodotServer {
|
|
|
36209
36469
|
if (!engine.ok) {
|
|
36210
36470
|
return engine.response;
|
|
36211
36471
|
}
|
|
36212
|
-
const application = parseProjectGodot(
|
|
36472
|
+
const application = parseProjectGodot(readFileSync20(project.value.file, "utf8"))["application"] ?? {};
|
|
36213
36473
|
const name = application["config/name"];
|
|
36214
36474
|
const mainScene = application["run/main_scene"];
|
|
36215
36475
|
const info = {
|
|
@@ -36265,18 +36525,18 @@ class GodotServer {
|
|
|
36265
36525
|
if (!engine.ok) {
|
|
36266
36526
|
return engine.response;
|
|
36267
36527
|
}
|
|
36268
|
-
const folder =
|
|
36528
|
+
const folder = dirname15(output.absolutePath);
|
|
36269
36529
|
try {
|
|
36270
|
-
|
|
36530
|
+
mkdirSync13(folder, { recursive: true });
|
|
36271
36531
|
} catch (error) {
|
|
36272
|
-
return this.createErrorResponse(`Cannot create ${
|
|
36532
|
+
return this.createErrorResponse(`Cannot create ${dirname15(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
36273
36533
|
}
|
|
36274
36534
|
const debug = readBoolean(args, "debug") ?? false;
|
|
36275
36535
|
const exportLogs = scratchDirectory("export");
|
|
36276
36536
|
const exportArgs = [
|
|
36277
36537
|
"--headless",
|
|
36278
36538
|
"--log-file",
|
|
36279
|
-
|
|
36539
|
+
join22(exportLogs, "engine.log"),
|
|
36280
36540
|
"--path",
|
|
36281
36541
|
project.value.path,
|
|
36282
36542
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -36284,7 +36544,7 @@ class GodotServer {
|
|
|
36284
36544
|
output.absolutePath
|
|
36285
36545
|
];
|
|
36286
36546
|
this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
|
|
36287
|
-
const writtenAt = () =>
|
|
36547
|
+
const writtenAt = () => existsSync17(output.absolutePath) ? statSync8(output.absolutePath).mtimeMs : null;
|
|
36288
36548
|
const before = writtenAt();
|
|
36289
36549
|
let ending;
|
|
36290
36550
|
try {
|
|
@@ -36297,7 +36557,38 @@ class GodotServer {
|
|
|
36297
36557
|
}
|
|
36298
36558
|
return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt());
|
|
36299
36559
|
}
|
|
36560
|
+
reportTestProgress(log) {
|
|
36561
|
+
const progress = callProgress();
|
|
36562
|
+
if (progress === undefined) {
|
|
36563
|
+
return;
|
|
36564
|
+
}
|
|
36565
|
+
const began = Date.now();
|
|
36566
|
+
let read = 0;
|
|
36567
|
+
let lastSent = began;
|
|
36568
|
+
const ticker = setInterval(() => {
|
|
36569
|
+
const entries = log.all;
|
|
36570
|
+
const finishedOne = entries.slice(read).some((entry) => finishesASuite(entry.text));
|
|
36571
|
+
read = entries.length;
|
|
36572
|
+
const now = Date.now();
|
|
36573
|
+
if (!finishedOne && now - lastSent < TEST_HEARTBEAT_MS) {
|
|
36574
|
+
return;
|
|
36575
|
+
}
|
|
36576
|
+
lastSent = now;
|
|
36577
|
+
progress(Math.floor((now - began) / 1000), testProgressMessage(entries.map((entry) => entry.text), now - began));
|
|
36578
|
+
}, 1000);
|
|
36579
|
+
ticker.unref();
|
|
36580
|
+
return ticker;
|
|
36581
|
+
}
|
|
36300
36582
|
async handleRunTests(args) {
|
|
36583
|
+
const answer = await this.runTests(args);
|
|
36584
|
+
const project = this.project(args);
|
|
36585
|
+
if (!project.ok || callSignal()?.aborted === true) {
|
|
36586
|
+
return answer;
|
|
36587
|
+
}
|
|
36588
|
+
const previous = takeCutTestRun(project.value.path);
|
|
36589
|
+
return previous === null ? answer : withPreviousCut(answer, previous);
|
|
36590
|
+
}
|
|
36591
|
+
async runTests(args) {
|
|
36301
36592
|
const project = this.project(args);
|
|
36302
36593
|
if (!project.ok) {
|
|
36303
36594
|
return project.response;
|
|
@@ -36306,7 +36597,7 @@ class GodotServer {
|
|
|
36306
36597
|
if (!paths.ok) {
|
|
36307
36598
|
return this.createErrorResponse(paths.reason, PATH_SOLUTIONS);
|
|
36308
36599
|
}
|
|
36309
|
-
const missing = paths.value.filter((path) => !
|
|
36600
|
+
const missing = paths.value.filter((path) => !existsSync17(join22(project.value.path, path.slice("res://".length))));
|
|
36310
36601
|
const asked = paths.value.filter((path) => !missing.includes(path));
|
|
36311
36602
|
if (asked.length === 0) {
|
|
36312
36603
|
const verdict = `nothing at ${missing.join(" and ")}`;
|
|
@@ -36322,7 +36613,7 @@ class GodotServer {
|
|
|
36322
36613
|
};
|
|
36323
36614
|
}
|
|
36324
36615
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
36325
|
-
if (!
|
|
36616
|
+
if (!existsSync17(join22(project.value.path, runner))) {
|
|
36326
36617
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
36327
36618
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
36328
36619
|
]);
|
|
@@ -36367,6 +36658,7 @@ class GodotServer {
|
|
|
36367
36658
|
run.process.stderr?.on("data", () => {
|
|
36368
36659
|
lastSaid = Date.now();
|
|
36369
36660
|
});
|
|
36661
|
+
const reporting = this.reportTestProgress(run.log);
|
|
36370
36662
|
let silentForMs = 0;
|
|
36371
36663
|
const timedOut = await new Promise((resolve) => {
|
|
36372
36664
|
const timer = setTimeout(() => {
|
|
@@ -36383,25 +36675,26 @@ class GodotServer {
|
|
|
36383
36675
|
resolve(false);
|
|
36384
36676
|
});
|
|
36385
36677
|
});
|
|
36678
|
+
clearInterval(reporting);
|
|
36386
36679
|
if (timedOut && run.process.pid !== undefined) {
|
|
36387
36680
|
await untilGone([run.process.pid]);
|
|
36388
36681
|
}
|
|
36389
36682
|
await run.settled;
|
|
36390
|
-
const reportsDir =
|
|
36683
|
+
const reportsDir = join22(project.value.path, ".godot", ...ours.split("/"));
|
|
36391
36684
|
let report = null;
|
|
36392
36685
|
let reportProblem = null;
|
|
36393
36686
|
try {
|
|
36394
|
-
const written =
|
|
36687
|
+
const written = existsSync17(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
36395
36688
|
const newest = written.at(-1);
|
|
36396
36689
|
if (newest !== undefined) {
|
|
36397
|
-
report = parseJUnit(
|
|
36690
|
+
report = parseJUnit(readFileSync20(join22(reportsDir, newest, "results.xml"), "utf8"));
|
|
36398
36691
|
}
|
|
36399
36692
|
} catch (error) {
|
|
36400
36693
|
reportProblem = errorMessage(error);
|
|
36401
36694
|
} finally {
|
|
36402
36695
|
discard(reportsDir, userData);
|
|
36403
36696
|
try {
|
|
36404
|
-
rmdirSync2(
|
|
36697
|
+
rmdirSync2(dirname15(reportsDir));
|
|
36405
36698
|
} catch {}
|
|
36406
36699
|
}
|
|
36407
36700
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
|
|
@@ -36423,7 +36716,9 @@ class GodotServer {
|
|
|
36423
36716
|
const nothingRan = timedOut ? null : whyNoReport(said, asked.join(" and "));
|
|
36424
36717
|
const cutShort = timedOut ? timedOutVerdict(timeoutMs, silentForMs) : null;
|
|
36425
36718
|
const hung = cutShort?.hung ?? false;
|
|
36426
|
-
const
|
|
36719
|
+
const printedErrors = timedOut || exitCode !== 105 ? [] : scriptErrorsPrinted(said);
|
|
36720
|
+
const declaring = printedErrors.some((error) => unresolvedClassIn(error.message) !== null) ? await originsOf(printedErrors, declaredClasses(project.value.path), checkWithEngine(engine.value, project.value.path)) : { origins: [], unconfirmed: [] };
|
|
36721
|
+
const scriptErrors = [...declaring.origins, ...printedErrors];
|
|
36427
36722
|
const brokenScripts = new Set(scriptErrors.map((error) => error.path)).size;
|
|
36428
36723
|
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));
|
|
36429
36724
|
const nothingAt = missing.join(" and ");
|
|
@@ -36436,9 +36731,28 @@ class GodotServer {
|
|
|
36436
36731
|
printed: echoed.entries.map((entry) => entry.text),
|
|
36437
36732
|
...echoed.omitted > 0 ? { printedOmitted: echoed.omitted } : {}
|
|
36438
36733
|
};
|
|
36734
|
+
const sofar = report === null ? suitesPrinted(said) : null;
|
|
36439
36735
|
if (report === null) {
|
|
36440
|
-
const
|
|
36736
|
+
const finished = sofar?.finished ?? [];
|
|
36737
|
+
const running = sofar?.running ?? null;
|
|
36738
|
+
const failing = finished.filter((suite) => suite.errors + suite.failures > 0);
|
|
36739
|
+
const atTheEnd = {
|
|
36740
|
+
...nonEmpty({
|
|
36741
|
+
suitesFinished: finished.map((suite) => suite.path),
|
|
36742
|
+
suitesFailing: failing.map(({ path, errors, failures, failed }) => ({
|
|
36743
|
+
path,
|
|
36744
|
+
errors,
|
|
36745
|
+
failures,
|
|
36746
|
+
failed
|
|
36747
|
+
}))
|
|
36748
|
+
}),
|
|
36749
|
+
...running === null ? {} : { suiteRunning: running }
|
|
36750
|
+
};
|
|
36751
|
+
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}).${finishedBeforeTheEnd(finished.length, failing.length, running)}`;
|
|
36441
36752
|
const note = missingNote === undefined ? said : `${said} ${missingNote}`;
|
|
36753
|
+
if (callSignal()?.aborted === true) {
|
|
36754
|
+
keepCutTestRun(project.value.path, { cutAt: new Date().toISOString(), paths: asked, ...atTheEnd });
|
|
36755
|
+
}
|
|
36442
36756
|
return {
|
|
36443
36757
|
content: [
|
|
36444
36758
|
{ type: "text", text: note },
|
|
@@ -36454,8 +36768,9 @@ class GodotServer {
|
|
|
36454
36768
|
...timedOut ? { timedOut, silentForMs } : {},
|
|
36455
36769
|
...missing.length > 0 ? { missing } : {},
|
|
36456
36770
|
...nonEmpty(foldedScriptErrors(scriptErrors)),
|
|
36771
|
+
...atTheEnd,
|
|
36457
36772
|
arguments: cmdArgs,
|
|
36458
|
-
entries: forAnswer(printed.slice(0, 60).map(aboveTheRunner)),
|
|
36773
|
+
entries: forAnswer(withoutListed(printed, scriptErrors).slice(0, 60).map(aboveTheRunner)),
|
|
36459
36774
|
...printedLines,
|
|
36460
36775
|
savesNote,
|
|
36461
36776
|
...scanned
|
|
@@ -36588,11 +36903,11 @@ class GodotServer {
|
|
|
36588
36903
|
const cached = cachedClasses(projectPath);
|
|
36589
36904
|
const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
|
|
36590
36905
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
36591
|
-
if (!contained.ok || !
|
|
36906
|
+
if (!contained.ok || !existsSync17(contained.absolutePath)) {
|
|
36592
36907
|
return null;
|
|
36593
36908
|
}
|
|
36594
36909
|
try {
|
|
36595
|
-
return
|
|
36910
|
+
return readFileSync20(contained.absolutePath, "utf8");
|
|
36596
36911
|
} catch {
|
|
36597
36912
|
return null;
|
|
36598
36913
|
}
|
|
@@ -36648,11 +36963,11 @@ class GodotServer {
|
|
|
36648
36963
|
}
|
|
36649
36964
|
sourceOfClass(projectPath, resourcePath) {
|
|
36650
36965
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
36651
|
-
if (!contained.ok || !
|
|
36966
|
+
if (!contained.ok || !existsSync17(contained.absolutePath)) {
|
|
36652
36967
|
return null;
|
|
36653
36968
|
}
|
|
36654
36969
|
try {
|
|
36655
|
-
return
|
|
36970
|
+
return readFileSync20(contained.absolutePath, "utf8");
|
|
36656
36971
|
} catch {
|
|
36657
36972
|
return null;
|
|
36658
36973
|
}
|
|
@@ -36963,7 +37278,7 @@ class GodotServer {
|
|
|
36963
37278
|
return null;
|
|
36964
37279
|
}
|
|
36965
37280
|
try {
|
|
36966
|
-
return settingKeys(
|
|
37281
|
+
return settingKeys(readFileSync20(join22(projectPath, "project.godot"), "utf8"));
|
|
36967
37282
|
} catch {
|
|
36968
37283
|
return null;
|
|
36969
37284
|
}
|
|
@@ -37285,7 +37600,7 @@ class GodotServer {
|
|
|
37285
37600
|
}
|
|
37286
37601
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
37287
37602
|
const budgetMs = budget.ms;
|
|
37288
|
-
if (!
|
|
37603
|
+
if (!existsSync17(join22(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
37289
37604
|
return runtimeVerdict(null, {
|
|
37290
37605
|
addon: false,
|
|
37291
37606
|
budgetMs,
|
|
@@ -37544,7 +37859,7 @@ class GodotServer {
|
|
|
37544
37859
|
return 0;
|
|
37545
37860
|
}
|
|
37546
37861
|
try {
|
|
37547
|
-
return reportAlreadyInTranscript(
|
|
37862
|
+
return reportAlreadyInTranscript(readFileSync20(game.transcript), readFileSync20(report));
|
|
37548
37863
|
} catch (error) {
|
|
37549
37864
|
this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
|
|
37550
37865
|
return 0;
|
|
@@ -38302,7 +38617,7 @@ class GodotServer {
|
|
|
38302
38617
|
notes.push(endedWithoutACode({
|
|
38303
38618
|
throughEditor: run.throughEditor,
|
|
38304
38619
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
38305
|
-
couldAnnounce: run.projectPath !== null &&
|
|
38620
|
+
couldAnnounce: run.projectPath !== null && existsSync17(join22(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
38306
38621
|
endedHere: typeof run.endedHere === "string",
|
|
38307
38622
|
pickedUp: run.pickedUp === true
|
|
38308
38623
|
}));
|
|
@@ -38863,7 +39178,7 @@ class GodotServer {
|
|
|
38863
39178
|
if (!scriptPath.ok) {
|
|
38864
39179
|
return this.createErrorResponse(scriptPath.reason);
|
|
38865
39180
|
}
|
|
38866
|
-
if (!
|
|
39181
|
+
if (!existsSync17(scriptPath.absolutePath)) {
|
|
38867
39182
|
return this.createErrorResponse(`Script file does not exist: ${scriptPath.absolutePath}`);
|
|
38868
39183
|
}
|
|
38869
39184
|
const asked = readNonEmptyString(args, "newScriptPath");
|
|
@@ -39151,7 +39466,7 @@ class GodotServer {
|
|
|
39151
39466
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
39152
39467
|
const run = await this.currentRun();
|
|
39153
39468
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
39154
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !
|
|
39469
|
+
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync17(join22(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
|
|
39155
39470
|
return null;
|
|
39156
39471
|
}
|
|
39157
39472
|
const already = runtimesAnnounced().running;
|
|
@@ -39235,7 +39550,7 @@ class GodotServer {
|
|
|
39235
39550
|
}
|
|
39236
39551
|
}
|
|
39237
39552
|
const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
|
|
39238
|
-
const screenshotPath = screenshotDir ?
|
|
39553
|
+
const screenshotPath = screenshotDir ? join22(screenshotDir, "capture.png") : null;
|
|
39239
39554
|
const staleNote = this.runtimeStaleNote(choice.endpoint);
|
|
39240
39555
|
const stale = staleNote === undefined ? {} : { addonIsStale: true, staleNote };
|
|
39241
39556
|
const refused = (message) => this.createErrorResponse(staleNote === undefined ? message : `${message}
|
|
@@ -39284,7 +39599,7 @@ ${staleNote}`);
|
|
|
39284
39599
|
|
|
39285
39600
|
${staleNote}`}`
|
|
39286
39601
|
},
|
|
39287
|
-
{ type: "image", data:
|
|
39602
|
+
{ type: "image", data: readFileSync20(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
39288
39603
|
]
|
|
39289
39604
|
};
|
|
39290
39605
|
} finally {
|