gdharness 1.1.54 → 1.1.56
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 +152 -34
- package/build/index.js +152 -32
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -7840,10 +7840,54 @@ function announced(line) {
|
|
|
7840
7840
|
if (!match) {
|
|
7841
7841
|
return null;
|
|
7842
7842
|
}
|
|
7843
|
-
return {
|
|
7843
|
+
return {
|
|
7844
|
+
severity: match[2] === "WARNING" ? "warning" : "error",
|
|
7845
|
+
text: match[3] ?? "",
|
|
7846
|
+
byScript: match[2] === "SCRIPT ERROR"
|
|
7847
|
+
};
|
|
7848
|
+
}
|
|
7849
|
+
function reported(entry) {
|
|
7850
|
+
return entry.detail.length === 0 ? { index: entry.index, severity: entry.severity, source: entry.source, text: entry.text } : { ...entry, detail: framesCollapsed(entry.detail) };
|
|
7851
|
+
}
|
|
7852
|
+
function framesCollapsed(detail) {
|
|
7853
|
+
const out = [];
|
|
7854
|
+
let pending;
|
|
7855
|
+
for (const line of detail) {
|
|
7856
|
+
const match = FRAME.exec(line);
|
|
7857
|
+
const number = match?.[1];
|
|
7858
|
+
const frame = match?.[2];
|
|
7859
|
+
if (frame !== undefined && pending?.frame === frame && Number(number) === Number(pending.last) + 1) {
|
|
7860
|
+
pending.last = String(number);
|
|
7861
|
+
continue;
|
|
7862
|
+
}
|
|
7863
|
+
if (pending !== undefined) {
|
|
7864
|
+
out.push(writtenRun(pending));
|
|
7865
|
+
}
|
|
7866
|
+
pending = number === undefined || frame === undefined ? undefined : { first: number, last: number, frame };
|
|
7867
|
+
if (pending === undefined) {
|
|
7868
|
+
out.push(line);
|
|
7869
|
+
}
|
|
7870
|
+
}
|
|
7871
|
+
if (pending !== undefined) {
|
|
7872
|
+
out.push(writtenRun(pending));
|
|
7873
|
+
}
|
|
7874
|
+
return out;
|
|
7844
7875
|
}
|
|
7845
|
-
function
|
|
7846
|
-
return
|
|
7876
|
+
function writtenRun(run) {
|
|
7877
|
+
return run.first === run.last ? `[${run.first}] ${run.frame}` : `[${run.first}-${run.last}] ${run.frame}`;
|
|
7878
|
+
}
|
|
7879
|
+
function foldedRepeats(entries) {
|
|
7880
|
+
const first = new Map;
|
|
7881
|
+
for (const entry of entries) {
|
|
7882
|
+
const key = JSON.stringify([entry.severity, entry.source, entry.text, entry.detail]);
|
|
7883
|
+
const seen = first.get(key);
|
|
7884
|
+
if (seen === undefined) {
|
|
7885
|
+
first.set(key, { entry, times: 1 });
|
|
7886
|
+
} else {
|
|
7887
|
+
seen.times += 1;
|
|
7888
|
+
}
|
|
7889
|
+
}
|
|
7890
|
+
return [...first.values()].map(({ entry, times }) => times === 1 ? reported(entry) : { ...reported(entry), times });
|
|
7847
7891
|
}
|
|
7848
7892
|
|
|
7849
7893
|
class GameLog {
|
|
@@ -7854,8 +7898,13 @@ class GameLog {
|
|
|
7854
7898
|
transcript: new StringDecoder("utf8")
|
|
7855
7899
|
};
|
|
7856
7900
|
partial = { stdout: "", stderr: "", transcript: "" };
|
|
7857
|
-
lastHeadline =
|
|
7901
|
+
lastHeadline = {
|
|
7902
|
+
stdout: null,
|
|
7903
|
+
stderr: null,
|
|
7904
|
+
transcript: null
|
|
7905
|
+
};
|
|
7858
7906
|
seen = { error: 0, warning: 0, info: 0 };
|
|
7907
|
+
byScript = new Set;
|
|
7859
7908
|
append(source, chunk) {
|
|
7860
7909
|
const decoded = typeof chunk === "string" ? chunk : this.decoders[source].write(chunk);
|
|
7861
7910
|
const lines = (this.partial[source] + decoded).split(`
|
|
@@ -7879,8 +7928,9 @@ class GameLog {
|
|
|
7879
7928
|
if (line.trim() === "") {
|
|
7880
7929
|
return;
|
|
7881
7930
|
}
|
|
7882
|
-
|
|
7883
|
-
|
|
7931
|
+
const owner = this.lastHeadline[source];
|
|
7932
|
+
if (owner !== null && /^\s/.test(line)) {
|
|
7933
|
+
owner.detail.push(line.trim());
|
|
7884
7934
|
return;
|
|
7885
7935
|
}
|
|
7886
7936
|
const headline = announced(line);
|
|
@@ -7893,7 +7943,13 @@ class GameLog {
|
|
|
7893
7943
|
detail
|
|
7894
7944
|
};
|
|
7895
7945
|
this.entries.push(entry);
|
|
7896
|
-
|
|
7946
|
+
if (headline?.byScript === true) {
|
|
7947
|
+
this.byScript.add(entry.index);
|
|
7948
|
+
}
|
|
7949
|
+
this.lastHeadline[source] = headline ? { detail } : null;
|
|
7950
|
+
}
|
|
7951
|
+
raisedByScript(entry) {
|
|
7952
|
+
return this.byScript.has(entry.index);
|
|
7897
7953
|
}
|
|
7898
7954
|
record(severity, text) {
|
|
7899
7955
|
this.entries.push({
|
|
@@ -7903,7 +7959,6 @@ class GameLog {
|
|
|
7903
7959
|
text,
|
|
7904
7960
|
detail: []
|
|
7905
7961
|
});
|
|
7906
|
-
this.lastHeadline = null;
|
|
7907
7962
|
}
|
|
7908
7963
|
get all() {
|
|
7909
7964
|
return this.entries;
|
|
@@ -7927,6 +7982,11 @@ class GameLog {
|
|
|
7927
7982
|
const omitted = Math.max(0, matching.length - options.limit);
|
|
7928
7983
|
return { entries: matching.slice(omitted), omitted };
|
|
7929
7984
|
}
|
|
7985
|
+
selectFolded(options) {
|
|
7986
|
+
const folded = foldedRepeats(this.select({ ...options, limit: Number.POSITIVE_INFINITY }).entries);
|
|
7987
|
+
const omitted = Math.max(0, folded.length - options.limit);
|
|
7988
|
+
return { entries: folded.slice(omitted), omitted };
|
|
7989
|
+
}
|
|
7930
7990
|
}
|
|
7931
7991
|
function answersTo(entry, severity, contains) {
|
|
7932
7992
|
if (!ADMITTED[severity].has(entry.severity)) {
|
|
@@ -7938,7 +7998,7 @@ function answersTo(entry, severity, contains) {
|
|
|
7938
7998
|
const needle = contains.toLowerCase();
|
|
7939
7999
|
return [entry.text, ...entry.detail].some((line) => line.toLowerCase().includes(needle));
|
|
7940
8000
|
}
|
|
7941
|
-
var HEADLINE, COLOUR_CODE, STREAMS, ADMITTED;
|
|
8001
|
+
var HEADLINE, COLOUR_CODE, STREAMS, ADMITTED, FRAME;
|
|
7942
8002
|
var init_game_log = __esm(() => {
|
|
7943
8003
|
HEADLINE = /^(USER )?(SCRIPT ERROR|SHADER ERROR|ERROR|WARNING):\s?(.*)$/;
|
|
7944
8004
|
COLOUR_CODE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
@@ -7948,6 +8008,7 @@ var init_game_log = __esm(() => {
|
|
|
7948
8008
|
warning: new Set(["error", "warning"]),
|
|
7949
8009
|
info: new Set(["error", "warning", "info"])
|
|
7950
8010
|
};
|
|
8011
|
+
FRAME = /^\[(\d+)\] (.+)$/;
|
|
7951
8012
|
});
|
|
7952
8013
|
|
|
7953
8014
|
// src/engine-run.ts
|
|
@@ -8166,7 +8227,7 @@ function problemsIn(log) {
|
|
|
8166
8227
|
return [];
|
|
8167
8228
|
});
|
|
8168
8229
|
}
|
|
8169
|
-
function
|
|
8230
|
+
function reported2(problems) {
|
|
8170
8231
|
const omitted = Math.max(0, problems.length - MESSAGES_KEPT);
|
|
8171
8232
|
return omitted === 0 ? { messages: problems } : { messages: problems.slice(omitted), messagesOmitted: omitted };
|
|
8172
8233
|
}
|
|
@@ -8251,12 +8312,12 @@ async function importOnce(godotPath, projectPath) {
|
|
|
8251
8312
|
}
|
|
8252
8313
|
const problems = problemsIn(ran.log);
|
|
8253
8314
|
if (ran.exitCode === 0 && ran.failure === null) {
|
|
8254
|
-
return { ok: true, ...
|
|
8315
|
+
return { ok: true, ...reported2(problems) };
|
|
8255
8316
|
}
|
|
8256
8317
|
return {
|
|
8257
8318
|
ok: false,
|
|
8258
8319
|
message: `the import pass failed (${howItEnded(ran)}): ${reason(ran.log, problems)}`,
|
|
8259
|
-
...
|
|
8320
|
+
...reported2(problems)
|
|
8260
8321
|
};
|
|
8261
8322
|
}
|
|
8262
8323
|
async function runOperation(engine, operation, params, projectPath) {
|
|
@@ -8290,7 +8351,7 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
8290
8351
|
return {
|
|
8291
8352
|
ok: false,
|
|
8292
8353
|
message: `${operation} wrote an answer that could not be read (${answer.why})`,
|
|
8293
|
-
...
|
|
8354
|
+
...reported2(problems)
|
|
8294
8355
|
};
|
|
8295
8356
|
}
|
|
8296
8357
|
if (answer.kind === "none") {
|
|
@@ -8298,7 +8359,7 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
8298
8359
|
return {
|
|
8299
8360
|
ok: false,
|
|
8300
8361
|
message: `${operation} ${ending}: ${reason(ran.log, problems)}`,
|
|
8301
|
-
...
|
|
8362
|
+
...reported2(problems)
|
|
8302
8363
|
};
|
|
8303
8364
|
}
|
|
8304
8365
|
const fault = scriptFault(problems, engine.script);
|
|
@@ -8306,14 +8367,14 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
8306
8367
|
return {
|
|
8307
8368
|
ok: false,
|
|
8308
8369
|
message: `${operation} hit an error in the operations script, so its answer is left out: part of it was ` + `never computed. ${fault.text} (${fault.detail[0]?.slice("at: ".length) ?? "no location given"})`,
|
|
8309
|
-
...
|
|
8370
|
+
...reported2(problems)
|
|
8310
8371
|
};
|
|
8311
8372
|
}
|
|
8312
8373
|
const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). Anything it reported on the way out is under engine_messages.`;
|
|
8313
8374
|
return {
|
|
8314
8375
|
ok: true,
|
|
8315
8376
|
payload: answer.payload,
|
|
8316
|
-
...
|
|
8377
|
+
...reported2(problems),
|
|
8317
8378
|
...afterAnswer === undefined ? {} : { afterAnswer }
|
|
8318
8379
|
};
|
|
8319
8380
|
}
|
|
@@ -17646,7 +17707,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17646
17707
|
},
|
|
17647
17708
|
{
|
|
17648
17709
|
name: "project_test",
|
|
17649
|
-
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.",
|
|
17710
|
+
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; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. 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. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
|
|
17650
17711
|
parameters: {
|
|
17651
17712
|
projectPath: PROJECT_PATH,
|
|
17652
17713
|
path: {
|
|
@@ -18094,7 +18155,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18094
18155
|
limit: {
|
|
18095
18156
|
type: "integer",
|
|
18096
18157
|
minimum: 1,
|
|
18097
|
-
description: "The most entries to answer with, newest kept. Default 200, and omitted says how many matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
|
|
18158
|
+
description: "The most entries to answer with, newest kept. Identical entries come back as one, at the first of them, with times saying how many were printed, so the limit counts distinct entries. Default 200, and omitted says how many distinct matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
|
|
18098
18159
|
},
|
|
18099
18160
|
before: {
|
|
18100
18161
|
ops: ["editor"],
|
|
@@ -38515,7 +38576,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
38515
38576
|
case "godot_ready":
|
|
38516
38577
|
if (this.connectionInfo) {
|
|
38517
38578
|
if (this.belongsElsewhere(message.project_path)) {
|
|
38518
|
-
this.
|
|
38579
|
+
this.elsewhereUnlessOneShot(message.project_path, message.editor_pid, message.opened_by_a_server === true);
|
|
38519
38580
|
return;
|
|
38520
38581
|
}
|
|
38521
38582
|
this.connectionInfo.projectPath = message.project_path;
|
|
@@ -38626,6 +38687,34 @@ var init_godot_bridge = __esm(() => {
|
|
|
38626
38687
|
const fits = closeReason(said, "start") === said;
|
|
38627
38688
|
stranger?.close(OTHER_PROJECT_CLOSE_CODE, fits ? said : closeReason(`this server serves ${this.ownProject ?? ""}`, "end"));
|
|
38628
38689
|
}
|
|
38690
|
+
elsewhereUnlessOneShot(theirProject, pid, openedByAServer) {
|
|
38691
|
+
const known = pid === undefined ? undefined : this.oneShots.get(pid);
|
|
38692
|
+
if (pid !== undefined && known !== undefined) {
|
|
38693
|
+
this.turnAway(pid, known);
|
|
38694
|
+
return;
|
|
38695
|
+
}
|
|
38696
|
+
const info = this.connectionInfo;
|
|
38697
|
+
const socket = this.socket;
|
|
38698
|
+
const reader = this.oneShotReader;
|
|
38699
|
+
if (pid === undefined || openedByAServer || info === null || reader === null) {
|
|
38700
|
+
this.sendElsewhere(theirProject);
|
|
38701
|
+
return;
|
|
38702
|
+
}
|
|
38703
|
+
info.identifying = true;
|
|
38704
|
+
reader(pid).catch(() => {
|
|
38705
|
+
return;
|
|
38706
|
+
}).then((option) => {
|
|
38707
|
+
if (this.socket !== socket || this.connectionInfo !== info) {
|
|
38708
|
+
return;
|
|
38709
|
+
}
|
|
38710
|
+
if (typeof option === "string") {
|
|
38711
|
+
this.oneShots.set(pid, option);
|
|
38712
|
+
this.turnAway(pid, option);
|
|
38713
|
+
return;
|
|
38714
|
+
}
|
|
38715
|
+
this.sendElsewhere(theirProject);
|
|
38716
|
+
});
|
|
38717
|
+
}
|
|
38629
38718
|
identify(pid) {
|
|
38630
38719
|
const known = this.oneShots.get(pid);
|
|
38631
38720
|
if (known !== undefined) {
|
|
@@ -42563,7 +42652,6 @@ function notRunNote(notRun, failFast) {
|
|
|
42563
42652
|
function exportAnswer(ending, asked, before, after) {
|
|
42564
42653
|
const { log } = ending;
|
|
42565
42654
|
const written = after !== null && after !== before;
|
|
42566
|
-
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
42567
42655
|
const verdict = {
|
|
42568
42656
|
exported: ending.exitCode === 0 && ending.failure === null && log.count("error") === 0 && written,
|
|
42569
42657
|
...asked,
|
|
@@ -42572,8 +42660,7 @@ function exportAnswer(ending, asked, before, after) {
|
|
|
42572
42660
|
failure: ending.failure ?? undefined,
|
|
42573
42661
|
errors: log.count("error"),
|
|
42574
42662
|
warnings: log.count("warning"),
|
|
42575
|
-
|
|
42576
|
-
...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
|
|
42663
|
+
...foldedEntriesAnswer(log)
|
|
42577
42664
|
};
|
|
42578
42665
|
if (verdict.exported) {
|
|
42579
42666
|
return { content: [{ type: "text", text: answerJson(verdict) }] };
|
|
@@ -42779,6 +42866,32 @@ function aboveTheRunner(entry) {
|
|
|
42779
42866
|
]
|
|
42780
42867
|
};
|
|
42781
42868
|
}
|
|
42869
|
+
function foldedEntriesAnswer(log) {
|
|
42870
|
+
const problems = log.selectFolded({
|
|
42871
|
+
severity: "warning",
|
|
42872
|
+
sinceLastCall: false,
|
|
42873
|
+
limit: MOST_ENGINE_ENTRIES
|
|
42874
|
+
});
|
|
42875
|
+
return {
|
|
42876
|
+
entries: problems.entries,
|
|
42877
|
+
...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
|
|
42878
|
+
};
|
|
42879
|
+
}
|
|
42880
|
+
function gdUnitFailsOnScriptErrors(projectPath) {
|
|
42881
|
+
try {
|
|
42882
|
+
return settingKeys(readFileSync22(join26(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
|
|
42883
|
+
} catch {
|
|
42884
|
+
return true;
|
|
42885
|
+
}
|
|
42886
|
+
}
|
|
42887
|
+
function uncaughtNote(uncaught) {
|
|
42888
|
+
const first = uncaught[0];
|
|
42889
|
+
if (first === undefined) {
|
|
42890
|
+
return;
|
|
42891
|
+
}
|
|
42892
|
+
const count = uncaught.length === 1 ? "one script error" : `${uncaught.length} script errors`;
|
|
42893
|
+
return `Every case passed, but the run printed ${count} that no case failed on, the first "${first.text}"; ` + "they are under uncaughtScriptErrors. gdUnit4 fails a case on the script errors its logger sees, and " + "it did not see these, so the cases passed on whatever the faulted calls returned. A stack overflow is " + "one it cannot see: it is raised deeper than the logger can be called.";
|
|
42894
|
+
}
|
|
42782
42895
|
function elsewhereIn(projectPath, asked) {
|
|
42783
42896
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
42784
42897
|
if (wanted === "" || existsSync20(join26(projectPath, wanted))) {
|
|
@@ -44563,7 +44676,8 @@ class GodotServer {
|
|
|
44563
44676
|
rmdirSync2(dirname19(reportsDir));
|
|
44564
44677
|
} catch {}
|
|
44565
44678
|
}
|
|
44566
|
-
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit:
|
|
44679
|
+
const engineEntries = foldedRepeats(run.log.select({ severity: "warning", sinceLastCall: false, limit: Number.POSITIVE_INFINITY }).entries.map(aboveTheRunner));
|
|
44680
|
+
const engineEntriesShown = engineEntries.slice(-MOST_ENGINE_ENTRIES);
|
|
44567
44681
|
const verdicts = {
|
|
44568
44682
|
0: "passed",
|
|
44569
44683
|
100: "failures",
|
|
@@ -44573,6 +44687,7 @@ class GodotServer {
|
|
|
44573
44687
|
105: "script errors"
|
|
44574
44688
|
};
|
|
44575
44689
|
const exitCode = run.exitCode;
|
|
44690
|
+
const uncaught = report !== null && exitCode === 0 && !timedOut && gdUnitFailsOnScriptErrors(project.value.path) ? run.log.all.filter((entry) => run.log.raisedByScript(entry) && !entry.detail.some((line) => line.includes(ERROR_ASSERT))) : [];
|
|
44576
44691
|
const printed = run.log.select({
|
|
44577
44692
|
severity: "info",
|
|
44578
44693
|
sinceLastCall: false,
|
|
@@ -44586,7 +44701,7 @@ class GodotServer {
|
|
|
44586
44701
|
const declaring = printedErrors.some((error) => unresolvedClassIn(error.message) !== null) ? await originsOf(printedErrors, declaredClasses(project.value.path), checkWithEngine(engine.value, project.value.path)) : { origins: [], unconfirmed: [] };
|
|
44587
44702
|
const scriptErrors = [...declaring.origins, ...printedErrors];
|
|
44588
44703
|
const brokenScripts = new Set(scriptErrors.map((error) => error.path)).size;
|
|
44589
|
-
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));
|
|
44704
|
+
const ranAs = cutShort !== null ? cutShort.verdict : nothingRan ?? (brokenScripts > 0 ? `script errors in ${brokenScripts === 1 ? "one script" : `${brokenScripts} scripts`}, so no suite ran` : undefined) ?? (uncaught.length > 0 ? `${uncaught.length === 1 ? "one script error" : `${uncaught.length} script errors`} that failed no case` : undefined) ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
|
|
44590
44705
|
const nothingAt = missing.join(" and ");
|
|
44591
44706
|
const verdict = missing.length === 0 ? ranAs : `${ranAs}; nothing at ${nothingAt}`;
|
|
44592
44707
|
const missingNote = missing.length === 0 ? undefined : `Nothing at ${nothingAt}, so ${missing.length === 1 ? "it was" : "they were"} left out and only ${asked.join(", ")} ran; a run of fewer paths than were asked for is not called a pass.${elsewhereIn(project.value.path, missing[0] ?? "")}`;
|
|
@@ -44636,7 +44751,7 @@ class GodotServer {
|
|
|
44636
44751
|
...nonEmpty(foldedScriptErrors(scriptErrors)),
|
|
44637
44752
|
...atTheEnd,
|
|
44638
44753
|
arguments: cmdArgs,
|
|
44639
|
-
entries:
|
|
44754
|
+
entries: foldedRepeats(withoutListed(printed, scriptErrors).map(aboveTheRunner)).slice(0, 60),
|
|
44640
44755
|
...printedLines,
|
|
44641
44756
|
savesNote,
|
|
44642
44757
|
...scanned
|
|
@@ -44671,7 +44786,7 @@ class GodotServer {
|
|
|
44671
44786
|
});
|
|
44672
44787
|
const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
|
|
44673
44788
|
return this.jsonTextResponse({
|
|
44674
|
-
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && missing.length === 0,
|
|
44789
|
+
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && missing.length === 0,
|
|
44675
44790
|
verdict,
|
|
44676
44791
|
...missing.length > 0 ? { missing } : {},
|
|
44677
44792
|
...timedOut ? { timedOut, hung, silentForMs } : {},
|
|
@@ -44684,6 +44799,7 @@ class GodotServer {
|
|
|
44684
44799
|
time: report.time,
|
|
44685
44800
|
failed,
|
|
44686
44801
|
hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
|
|
44802
|
+
uncaughtScriptErrors: uncaught.length > 0 ? foldedRepeats(uncaught.map(aboveTheRunner)) : undefined,
|
|
44687
44803
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
44688
44804
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
44689
44805
|
notRun: notRun > 0 ? notRun : undefined,
|
|
@@ -44692,6 +44808,7 @@ class GodotServer {
|
|
|
44692
44808
|
...scanned,
|
|
44693
44809
|
note: [
|
|
44694
44810
|
missingNote,
|
|
44811
|
+
uncaughtNote(uncaught),
|
|
44695
44812
|
ranAs.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRunNote(notRun, readBoolean(args, "failFast") === true)
|
|
44696
44813
|
].filter((part) => part !== undefined).join(" ") || undefined,
|
|
44697
44814
|
suites: unclean.map((suite) => ({
|
|
@@ -44709,7 +44826,8 @@ class GodotServer {
|
|
|
44709
44826
|
suitesPassed: report.suites.length - unclean.length,
|
|
44710
44827
|
engineErrors: run.log.count("error"),
|
|
44711
44828
|
engineWarnings: run.log.count("warning"),
|
|
44712
|
-
engineEntries,
|
|
44829
|
+
engineEntries: engineEntriesShown,
|
|
44830
|
+
engineEntriesOmitted: engineEntries.length > engineEntriesShown.length ? engineEntries.length - engineEntriesShown.length : undefined,
|
|
44713
44831
|
classes: withAddonClassesCounted(classes.payload, cachedClasses(project.value.path) ?? new Map)
|
|
44714
44832
|
});
|
|
44715
44833
|
}
|
|
@@ -46418,7 +46536,7 @@ class GodotServer {
|
|
|
46418
46536
|
warnings,
|
|
46419
46537
|
...scanWaitAnswer(scanned),
|
|
46420
46538
|
savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
46421
|
-
|
|
46539
|
+
...foldedEntriesAnswer(boot.log)
|
|
46422
46540
|
});
|
|
46423
46541
|
}
|
|
46424
46542
|
async handleWaitForRun(args) {
|
|
@@ -46502,7 +46620,7 @@ class GodotServer {
|
|
|
46502
46620
|
const asked = readString(args, "severity");
|
|
46503
46621
|
const severity = asked === "error" || asked === "warning" ? asked : "info";
|
|
46504
46622
|
const contains = readNonEmptyString(args, "contains");
|
|
46505
|
-
const selected = console2.log.
|
|
46623
|
+
const selected = console2.log.selectFolded({
|
|
46506
46624
|
severity,
|
|
46507
46625
|
sinceLastCall: false,
|
|
46508
46626
|
contains,
|
|
@@ -46531,7 +46649,7 @@ class GodotServer {
|
|
|
46531
46649
|
},
|
|
46532
46650
|
repeated: shown,
|
|
46533
46651
|
moreShapes: grouped.length > shown.length ? grouped.length - shown.length : undefined,
|
|
46534
|
-
entries:
|
|
46652
|
+
entries: selected.entries,
|
|
46535
46653
|
omitted: selected.omitted === 0 ? undefined : selected.omitted
|
|
46536
46654
|
});
|
|
46537
46655
|
}
|
|
@@ -46553,7 +46671,7 @@ class GodotServer {
|
|
|
46553
46671
|
run.endedUnwatched = noCodeWillCome(run);
|
|
46554
46672
|
}
|
|
46555
46673
|
const severity = readString(args, "severity");
|
|
46556
|
-
const selected = run.log.
|
|
46674
|
+
const selected = run.log.selectFolded({
|
|
46557
46675
|
severity: severity === "error" || severity === "warning" ? severity : "info",
|
|
46558
46676
|
sinceLastCall: readBoolean(args, "sinceLastCall") ?? false,
|
|
46559
46677
|
contains: readNonEmptyString(args, "contains"),
|
|
@@ -46619,7 +46737,7 @@ class GodotServer {
|
|
|
46619
46737
|
cpuSeconds,
|
|
46620
46738
|
note: notes.length > 0 ? notes.join(" ") : undefined,
|
|
46621
46739
|
omitted: selected.omitted,
|
|
46622
|
-
entries:
|
|
46740
|
+
entries: selected.entries
|
|
46623
46741
|
});
|
|
46624
46742
|
}
|
|
46625
46743
|
anotherProjectsRun(runsProject, args, act) {
|
|
@@ -46686,7 +46804,7 @@ class GodotServer {
|
|
|
46686
46804
|
clean: verdict.clean,
|
|
46687
46805
|
errorsUnread: errorsUnread(stopped) ? true : undefined,
|
|
46688
46806
|
note: verdict.withChildren ? `${verdict.note} ${aboutTheChildren(ended, stopped.throughEditor)}` : verdict.note,
|
|
46689
|
-
|
|
46807
|
+
...foldedEntriesAnswer(stopped.log)
|
|
46690
46808
|
});
|
|
46691
46809
|
}
|
|
46692
46810
|
async stopAnnouncedGame(pid, andChildren, args) {
|
|
@@ -47666,7 +47784,7 @@ async function runGodotServer() {
|
|
|
47666
47784
|
const server = new GodotServer;
|
|
47667
47785
|
await server.run();
|
|
47668
47786
|
}
|
|
47669
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", EXTERNAL_MEMBER = 'Could not resolve external class member "';
|
|
47787
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "';
|
|
47670
47788
|
var init_server3 = __esm(() => {
|
|
47671
47789
|
init_mcp();
|
|
47672
47790
|
init_stdio2();
|
package/build/index.js
CHANGED
|
@@ -27850,7 +27850,11 @@ function announced(line) {
|
|
|
27850
27850
|
if (!match) {
|
|
27851
27851
|
return null;
|
|
27852
27852
|
}
|
|
27853
|
-
return {
|
|
27853
|
+
return {
|
|
27854
|
+
severity: match[2] === "WARNING" ? "warning" : "error",
|
|
27855
|
+
text: match[3] ?? "",
|
|
27856
|
+
byScript: match[2] === "SCRIPT ERROR"
|
|
27857
|
+
};
|
|
27854
27858
|
}
|
|
27855
27859
|
var STREAMS = ["stdout", "stderr", "transcript"];
|
|
27856
27860
|
var ADMITTED = {
|
|
@@ -27858,8 +27862,49 @@ var ADMITTED = {
|
|
|
27858
27862
|
warning: new Set(["error", "warning"]),
|
|
27859
27863
|
info: new Set(["error", "warning", "info"])
|
|
27860
27864
|
};
|
|
27861
|
-
function
|
|
27862
|
-
return
|
|
27865
|
+
function reported(entry) {
|
|
27866
|
+
return entry.detail.length === 0 ? { index: entry.index, severity: entry.severity, source: entry.source, text: entry.text } : { ...entry, detail: framesCollapsed(entry.detail) };
|
|
27867
|
+
}
|
|
27868
|
+
var FRAME = /^\[(\d+)\] (.+)$/;
|
|
27869
|
+
function framesCollapsed(detail) {
|
|
27870
|
+
const out = [];
|
|
27871
|
+
let pending;
|
|
27872
|
+
for (const line of detail) {
|
|
27873
|
+
const match = FRAME.exec(line);
|
|
27874
|
+
const number = match?.[1];
|
|
27875
|
+
const frame = match?.[2];
|
|
27876
|
+
if (frame !== undefined && pending?.frame === frame && Number(number) === Number(pending.last) + 1) {
|
|
27877
|
+
pending.last = String(number);
|
|
27878
|
+
continue;
|
|
27879
|
+
}
|
|
27880
|
+
if (pending !== undefined) {
|
|
27881
|
+
out.push(writtenRun(pending));
|
|
27882
|
+
}
|
|
27883
|
+
pending = number === undefined || frame === undefined ? undefined : { first: number, last: number, frame };
|
|
27884
|
+
if (pending === undefined) {
|
|
27885
|
+
out.push(line);
|
|
27886
|
+
}
|
|
27887
|
+
}
|
|
27888
|
+
if (pending !== undefined) {
|
|
27889
|
+
out.push(writtenRun(pending));
|
|
27890
|
+
}
|
|
27891
|
+
return out;
|
|
27892
|
+
}
|
|
27893
|
+
function writtenRun(run) {
|
|
27894
|
+
return run.first === run.last ? `[${run.first}] ${run.frame}` : `[${run.first}-${run.last}] ${run.frame}`;
|
|
27895
|
+
}
|
|
27896
|
+
function foldedRepeats(entries) {
|
|
27897
|
+
const first = new Map;
|
|
27898
|
+
for (const entry of entries) {
|
|
27899
|
+
const key = JSON.stringify([entry.severity, entry.source, entry.text, entry.detail]);
|
|
27900
|
+
const seen = first.get(key);
|
|
27901
|
+
if (seen === undefined) {
|
|
27902
|
+
first.set(key, { entry, times: 1 });
|
|
27903
|
+
} else {
|
|
27904
|
+
seen.times += 1;
|
|
27905
|
+
}
|
|
27906
|
+
}
|
|
27907
|
+
return [...first.values()].map(({ entry, times }) => times === 1 ? reported(entry) : { ...reported(entry), times });
|
|
27863
27908
|
}
|
|
27864
27909
|
|
|
27865
27910
|
class GameLog {
|
|
@@ -27870,8 +27915,13 @@ class GameLog {
|
|
|
27870
27915
|
transcript: new StringDecoder("utf8")
|
|
27871
27916
|
};
|
|
27872
27917
|
partial = { stdout: "", stderr: "", transcript: "" };
|
|
27873
|
-
lastHeadline =
|
|
27918
|
+
lastHeadline = {
|
|
27919
|
+
stdout: null,
|
|
27920
|
+
stderr: null,
|
|
27921
|
+
transcript: null
|
|
27922
|
+
};
|
|
27874
27923
|
seen = { error: 0, warning: 0, info: 0 };
|
|
27924
|
+
byScript = new Set;
|
|
27875
27925
|
append(source, chunk) {
|
|
27876
27926
|
const decoded = typeof chunk === "string" ? chunk : this.decoders[source].write(chunk);
|
|
27877
27927
|
const lines = (this.partial[source] + decoded).split(`
|
|
@@ -27895,8 +27945,9 @@ class GameLog {
|
|
|
27895
27945
|
if (line.trim() === "") {
|
|
27896
27946
|
return;
|
|
27897
27947
|
}
|
|
27898
|
-
|
|
27899
|
-
|
|
27948
|
+
const owner = this.lastHeadline[source];
|
|
27949
|
+
if (owner !== null && /^\s/.test(line)) {
|
|
27950
|
+
owner.detail.push(line.trim());
|
|
27900
27951
|
return;
|
|
27901
27952
|
}
|
|
27902
27953
|
const headline = announced(line);
|
|
@@ -27909,7 +27960,13 @@ class GameLog {
|
|
|
27909
27960
|
detail
|
|
27910
27961
|
};
|
|
27911
27962
|
this.entries.push(entry);
|
|
27912
|
-
|
|
27963
|
+
if (headline?.byScript === true) {
|
|
27964
|
+
this.byScript.add(entry.index);
|
|
27965
|
+
}
|
|
27966
|
+
this.lastHeadline[source] = headline ? { detail } : null;
|
|
27967
|
+
}
|
|
27968
|
+
raisedByScript(entry) {
|
|
27969
|
+
return this.byScript.has(entry.index);
|
|
27913
27970
|
}
|
|
27914
27971
|
record(severity, text) {
|
|
27915
27972
|
this.entries.push({
|
|
@@ -27919,7 +27976,6 @@ class GameLog {
|
|
|
27919
27976
|
text,
|
|
27920
27977
|
detail: []
|
|
27921
27978
|
});
|
|
27922
|
-
this.lastHeadline = null;
|
|
27923
27979
|
}
|
|
27924
27980
|
get all() {
|
|
27925
27981
|
return this.entries;
|
|
@@ -27943,6 +27999,11 @@ class GameLog {
|
|
|
27943
27999
|
const omitted = Math.max(0, matching.length - options.limit);
|
|
27944
28000
|
return { entries: matching.slice(omitted), omitted };
|
|
27945
28001
|
}
|
|
28002
|
+
selectFolded(options) {
|
|
28003
|
+
const folded = foldedRepeats(this.select({ ...options, limit: Number.POSITIVE_INFINITY }).entries);
|
|
28004
|
+
const omitted = Math.max(0, folded.length - options.limit);
|
|
28005
|
+
return { entries: folded.slice(omitted), omitted };
|
|
28006
|
+
}
|
|
27946
28007
|
}
|
|
27947
28008
|
function answersTo(entry, severity, contains) {
|
|
27948
28009
|
if (!ADMITTED[severity].has(entry.severity)) {
|
|
@@ -28570,7 +28631,7 @@ class GodotBridge extends EventEmitter {
|
|
|
28570
28631
|
case "godot_ready":
|
|
28571
28632
|
if (this.connectionInfo) {
|
|
28572
28633
|
if (this.belongsElsewhere(message.project_path)) {
|
|
28573
|
-
this.
|
|
28634
|
+
this.elsewhereUnlessOneShot(message.project_path, message.editor_pid, message.opened_by_a_server === true);
|
|
28574
28635
|
return;
|
|
28575
28636
|
}
|
|
28576
28637
|
this.connectionInfo.projectPath = message.project_path;
|
|
@@ -28681,6 +28742,34 @@ class GodotBridge extends EventEmitter {
|
|
|
28681
28742
|
const fits = closeReason(said, "start") === said;
|
|
28682
28743
|
stranger?.close(OTHER_PROJECT_CLOSE_CODE, fits ? said : closeReason(`this server serves ${this.ownProject ?? ""}`, "end"));
|
|
28683
28744
|
}
|
|
28745
|
+
elsewhereUnlessOneShot(theirProject, pid, openedByAServer) {
|
|
28746
|
+
const known = pid === undefined ? undefined : this.oneShots.get(pid);
|
|
28747
|
+
if (pid !== undefined && known !== undefined) {
|
|
28748
|
+
this.turnAway(pid, known);
|
|
28749
|
+
return;
|
|
28750
|
+
}
|
|
28751
|
+
const info = this.connectionInfo;
|
|
28752
|
+
const socket = this.socket;
|
|
28753
|
+
const reader = this.oneShotReader;
|
|
28754
|
+
if (pid === undefined || openedByAServer || info === null || reader === null) {
|
|
28755
|
+
this.sendElsewhere(theirProject);
|
|
28756
|
+
return;
|
|
28757
|
+
}
|
|
28758
|
+
info.identifying = true;
|
|
28759
|
+
reader(pid).catch(() => {
|
|
28760
|
+
return;
|
|
28761
|
+
}).then((option) => {
|
|
28762
|
+
if (this.socket !== socket || this.connectionInfo !== info) {
|
|
28763
|
+
return;
|
|
28764
|
+
}
|
|
28765
|
+
if (typeof option === "string") {
|
|
28766
|
+
this.oneShots.set(pid, option);
|
|
28767
|
+
this.turnAway(pid, option);
|
|
28768
|
+
return;
|
|
28769
|
+
}
|
|
28770
|
+
this.sendElsewhere(theirProject);
|
|
28771
|
+
});
|
|
28772
|
+
}
|
|
28684
28773
|
identify(pid) {
|
|
28685
28774
|
const known = this.oneShots.get(pid);
|
|
28686
28775
|
if (known !== undefined) {
|
|
@@ -29118,7 +29207,7 @@ function problemsIn(log) {
|
|
|
29118
29207
|
return [];
|
|
29119
29208
|
});
|
|
29120
29209
|
}
|
|
29121
|
-
function
|
|
29210
|
+
function reported2(problems) {
|
|
29122
29211
|
const omitted = Math.max(0, problems.length - MESSAGES_KEPT);
|
|
29123
29212
|
return omitted === 0 ? { messages: problems } : { messages: problems.slice(omitted), messagesOmitted: omitted };
|
|
29124
29213
|
}
|
|
@@ -29203,12 +29292,12 @@ async function importOnce(godotPath, projectPath) {
|
|
|
29203
29292
|
}
|
|
29204
29293
|
const problems = problemsIn(ran.log);
|
|
29205
29294
|
if (ran.exitCode === 0 && ran.failure === null) {
|
|
29206
|
-
return { ok: true, ...
|
|
29295
|
+
return { ok: true, ...reported2(problems) };
|
|
29207
29296
|
}
|
|
29208
29297
|
return {
|
|
29209
29298
|
ok: false,
|
|
29210
29299
|
message: `the import pass failed (${howItEnded(ran)}): ${reason(ran.log, problems)}`,
|
|
29211
|
-
...
|
|
29300
|
+
...reported2(problems)
|
|
29212
29301
|
};
|
|
29213
29302
|
}
|
|
29214
29303
|
async function runOperation(engine, operation, params, projectPath) {
|
|
@@ -29242,7 +29331,7 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
29242
29331
|
return {
|
|
29243
29332
|
ok: false,
|
|
29244
29333
|
message: `${operation} wrote an answer that could not be read (${answer.why})`,
|
|
29245
|
-
...
|
|
29334
|
+
...reported2(problems)
|
|
29246
29335
|
};
|
|
29247
29336
|
}
|
|
29248
29337
|
if (answer.kind === "none") {
|
|
@@ -29250,7 +29339,7 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
29250
29339
|
return {
|
|
29251
29340
|
ok: false,
|
|
29252
29341
|
message: `${operation} ${ending}: ${reason(ran.log, problems)}`,
|
|
29253
|
-
...
|
|
29342
|
+
...reported2(problems)
|
|
29254
29343
|
};
|
|
29255
29344
|
}
|
|
29256
29345
|
const fault = scriptFault(problems, engine.script);
|
|
@@ -29258,14 +29347,14 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
29258
29347
|
return {
|
|
29259
29348
|
ok: false,
|
|
29260
29349
|
message: `${operation} hit an error in the operations script, so its answer is left out: part of it was ` + `never computed. ${fault.text} (${fault.detail[0]?.slice("at: ".length) ?? "no location given"})`,
|
|
29261
|
-
...
|
|
29350
|
+
...reported2(problems)
|
|
29262
29351
|
};
|
|
29263
29352
|
}
|
|
29264
29353
|
const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). Anything it reported on the way out is under engine_messages.`;
|
|
29265
29354
|
return {
|
|
29266
29355
|
ok: true,
|
|
29267
29356
|
payload: answer.payload,
|
|
29268
|
-
...
|
|
29357
|
+
...reported2(problems),
|
|
29269
29358
|
...afterAnswer === undefined ? {} : { afterAnswer }
|
|
29270
29359
|
};
|
|
29271
29360
|
}
|
|
@@ -33500,7 +33589,7 @@ var TOOL_SPECS = [
|
|
|
33500
33589
|
},
|
|
33501
33590
|
{
|
|
33502
33591
|
name: "project_test",
|
|
33503
|
-
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.",
|
|
33592
|
+
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; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. 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. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
|
|
33504
33593
|
parameters: {
|
|
33505
33594
|
projectPath: PROJECT_PATH,
|
|
33506
33595
|
path: {
|
|
@@ -33948,7 +34037,7 @@ var TOOL_SPECS = [
|
|
|
33948
34037
|
limit: {
|
|
33949
34038
|
type: "integer",
|
|
33950
34039
|
minimum: 1,
|
|
33951
|
-
description: "The most entries to answer with, newest kept. Default 200, and omitted says how many matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
|
|
34040
|
+
description: "The most entries to answer with, newest kept. Identical entries come back as one, at the first of them, with times saying how many were printed, so the limit counts distinct entries. Default 200, and omitted says how many distinct matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
|
|
33952
34041
|
},
|
|
33953
34042
|
before: {
|
|
33954
34043
|
ops: ["editor"],
|
|
@@ -35010,7 +35099,6 @@ function notRunNote(notRun, failFast) {
|
|
|
35010
35099
|
function exportAnswer(ending, asked, before, after) {
|
|
35011
35100
|
const { log } = ending;
|
|
35012
35101
|
const written = after !== null && after !== before;
|
|
35013
|
-
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
35014
35102
|
const verdict = {
|
|
35015
35103
|
exported: ending.exitCode === 0 && ending.failure === null && log.count("error") === 0 && written,
|
|
35016
35104
|
...asked,
|
|
@@ -35019,8 +35107,7 @@ function exportAnswer(ending, asked, before, after) {
|
|
|
35019
35107
|
failure: ending.failure ?? undefined,
|
|
35020
35108
|
errors: log.count("error"),
|
|
35021
35109
|
warnings: log.count("warning"),
|
|
35022
|
-
|
|
35023
|
-
...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
|
|
35110
|
+
...foldedEntriesAnswer(log)
|
|
35024
35111
|
};
|
|
35025
35112
|
if (verdict.exported) {
|
|
35026
35113
|
return { content: [{ type: "text", text: answerJson(verdict) }] };
|
|
@@ -35270,6 +35357,34 @@ function aboveTheRunner(entry) {
|
|
|
35270
35357
|
]
|
|
35271
35358
|
};
|
|
35272
35359
|
}
|
|
35360
|
+
var MOST_ENGINE_ENTRIES = 200;
|
|
35361
|
+
function foldedEntriesAnswer(log) {
|
|
35362
|
+
const problems = log.selectFolded({
|
|
35363
|
+
severity: "warning",
|
|
35364
|
+
sinceLastCall: false,
|
|
35365
|
+
limit: MOST_ENGINE_ENTRIES
|
|
35366
|
+
});
|
|
35367
|
+
return {
|
|
35368
|
+
entries: problems.entries,
|
|
35369
|
+
...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
|
|
35370
|
+
};
|
|
35371
|
+
}
|
|
35372
|
+
var ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd";
|
|
35373
|
+
function gdUnitFailsOnScriptErrors(projectPath) {
|
|
35374
|
+
try {
|
|
35375
|
+
return settingKeys(readFileSync20(join22(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
|
|
35376
|
+
} catch {
|
|
35377
|
+
return true;
|
|
35378
|
+
}
|
|
35379
|
+
}
|
|
35380
|
+
function uncaughtNote(uncaught) {
|
|
35381
|
+
const first = uncaught[0];
|
|
35382
|
+
if (first === undefined) {
|
|
35383
|
+
return;
|
|
35384
|
+
}
|
|
35385
|
+
const count = uncaught.length === 1 ? "one script error" : `${uncaught.length} script errors`;
|
|
35386
|
+
return `Every case passed, but the run printed ${count} that no case failed on, the first "${first.text}"; ` + "they are under uncaughtScriptErrors. gdUnit4 fails a case on the script errors its logger sees, and " + "it did not see these, so the cases passed on whatever the faulted calls returned. A stack overflow is " + "one it cannot see: it is raised deeper than the logger can be called.";
|
|
35387
|
+
}
|
|
35273
35388
|
function elsewhereIn(projectPath, asked) {
|
|
35274
35389
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
35275
35390
|
if (wanted === "" || existsSync17(join22(projectPath, wanted))) {
|
|
@@ -37055,7 +37170,8 @@ class GodotServer {
|
|
|
37055
37170
|
rmdirSync2(dirname15(reportsDir));
|
|
37056
37171
|
} catch {}
|
|
37057
37172
|
}
|
|
37058
|
-
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit:
|
|
37173
|
+
const engineEntries = foldedRepeats(run.log.select({ severity: "warning", sinceLastCall: false, limit: Number.POSITIVE_INFINITY }).entries.map(aboveTheRunner));
|
|
37174
|
+
const engineEntriesShown = engineEntries.slice(-MOST_ENGINE_ENTRIES);
|
|
37059
37175
|
const verdicts = {
|
|
37060
37176
|
0: "passed",
|
|
37061
37177
|
100: "failures",
|
|
@@ -37065,6 +37181,7 @@ class GodotServer {
|
|
|
37065
37181
|
105: "script errors"
|
|
37066
37182
|
};
|
|
37067
37183
|
const exitCode = run.exitCode;
|
|
37184
|
+
const uncaught = report !== null && exitCode === 0 && !timedOut && gdUnitFailsOnScriptErrors(project.value.path) ? run.log.all.filter((entry) => run.log.raisedByScript(entry) && !entry.detail.some((line) => line.includes(ERROR_ASSERT))) : [];
|
|
37068
37185
|
const printed = run.log.select({
|
|
37069
37186
|
severity: "info",
|
|
37070
37187
|
sinceLastCall: false,
|
|
@@ -37078,7 +37195,7 @@ class GodotServer {
|
|
|
37078
37195
|
const declaring = printedErrors.some((error) => unresolvedClassIn(error.message) !== null) ? await originsOf(printedErrors, declaredClasses(project.value.path), checkWithEngine(engine.value, project.value.path)) : { origins: [], unconfirmed: [] };
|
|
37079
37196
|
const scriptErrors = [...declaring.origins, ...printedErrors];
|
|
37080
37197
|
const brokenScripts = new Set(scriptErrors.map((error) => error.path)).size;
|
|
37081
|
-
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));
|
|
37198
|
+
const ranAs = cutShort !== null ? cutShort.verdict : nothingRan ?? (brokenScripts > 0 ? `script errors in ${brokenScripts === 1 ? "one script" : `${brokenScripts} scripts`}, so no suite ran` : undefined) ?? (uncaught.length > 0 ? `${uncaught.length === 1 ? "one script error" : `${uncaught.length} script errors`} that failed no case` : undefined) ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
|
|
37082
37199
|
const nothingAt = missing.join(" and ");
|
|
37083
37200
|
const verdict = missing.length === 0 ? ranAs : `${ranAs}; nothing at ${nothingAt}`;
|
|
37084
37201
|
const missingNote = missing.length === 0 ? undefined : `Nothing at ${nothingAt}, so ${missing.length === 1 ? "it was" : "they were"} left out and only ${asked.join(", ")} ran; a run of fewer paths than were asked for is not called a pass.${elsewhereIn(project.value.path, missing[0] ?? "")}`;
|
|
@@ -37128,7 +37245,7 @@ class GodotServer {
|
|
|
37128
37245
|
...nonEmpty(foldedScriptErrors(scriptErrors)),
|
|
37129
37246
|
...atTheEnd,
|
|
37130
37247
|
arguments: cmdArgs,
|
|
37131
|
-
entries:
|
|
37248
|
+
entries: foldedRepeats(withoutListed(printed, scriptErrors).map(aboveTheRunner)).slice(0, 60),
|
|
37132
37249
|
...printedLines,
|
|
37133
37250
|
savesNote,
|
|
37134
37251
|
...scanned
|
|
@@ -37163,7 +37280,7 @@ class GodotServer {
|
|
|
37163
37280
|
});
|
|
37164
37281
|
const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
|
|
37165
37282
|
return this.jsonTextResponse({
|
|
37166
|
-
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && missing.length === 0,
|
|
37283
|
+
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && missing.length === 0,
|
|
37167
37284
|
verdict,
|
|
37168
37285
|
...missing.length > 0 ? { missing } : {},
|
|
37169
37286
|
...timedOut ? { timedOut, hung, silentForMs } : {},
|
|
@@ -37176,6 +37293,7 @@ class GodotServer {
|
|
|
37176
37293
|
time: report.time,
|
|
37177
37294
|
failed,
|
|
37178
37295
|
hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
|
|
37296
|
+
uncaughtScriptErrors: uncaught.length > 0 ? foldedRepeats(uncaught.map(aboveTheRunner)) : undefined,
|
|
37179
37297
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
37180
37298
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
37181
37299
|
notRun: notRun > 0 ? notRun : undefined,
|
|
@@ -37184,6 +37302,7 @@ class GodotServer {
|
|
|
37184
37302
|
...scanned,
|
|
37185
37303
|
note: [
|
|
37186
37304
|
missingNote,
|
|
37305
|
+
uncaughtNote(uncaught),
|
|
37187
37306
|
ranAs.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRunNote(notRun, readBoolean(args, "failFast") === true)
|
|
37188
37307
|
].filter((part) => part !== undefined).join(" ") || undefined,
|
|
37189
37308
|
suites: unclean.map((suite) => ({
|
|
@@ -37201,7 +37320,8 @@ class GodotServer {
|
|
|
37201
37320
|
suitesPassed: report.suites.length - unclean.length,
|
|
37202
37321
|
engineErrors: run.log.count("error"),
|
|
37203
37322
|
engineWarnings: run.log.count("warning"),
|
|
37204
|
-
engineEntries,
|
|
37323
|
+
engineEntries: engineEntriesShown,
|
|
37324
|
+
engineEntriesOmitted: engineEntries.length > engineEntriesShown.length ? engineEntries.length - engineEntriesShown.length : undefined,
|
|
37205
37325
|
classes: withAddonClassesCounted(classes.payload, cachedClasses(project.value.path) ?? new Map)
|
|
37206
37326
|
});
|
|
37207
37327
|
}
|
|
@@ -38910,7 +39030,7 @@ class GodotServer {
|
|
|
38910
39030
|
warnings,
|
|
38911
39031
|
...scanWaitAnswer(scanned),
|
|
38912
39032
|
savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
|
|
38913
|
-
|
|
39033
|
+
...foldedEntriesAnswer(boot.log)
|
|
38914
39034
|
});
|
|
38915
39035
|
}
|
|
38916
39036
|
async handleWaitForRun(args) {
|
|
@@ -38994,7 +39114,7 @@ class GodotServer {
|
|
|
38994
39114
|
const asked = readString(args, "severity");
|
|
38995
39115
|
const severity = asked === "error" || asked === "warning" ? asked : "info";
|
|
38996
39116
|
const contains = readNonEmptyString(args, "contains");
|
|
38997
|
-
const selected = console2.log.
|
|
39117
|
+
const selected = console2.log.selectFolded({
|
|
38998
39118
|
severity,
|
|
38999
39119
|
sinceLastCall: false,
|
|
39000
39120
|
contains,
|
|
@@ -39023,7 +39143,7 @@ class GodotServer {
|
|
|
39023
39143
|
},
|
|
39024
39144
|
repeated: shown,
|
|
39025
39145
|
moreShapes: grouped.length > shown.length ? grouped.length - shown.length : undefined,
|
|
39026
|
-
entries:
|
|
39146
|
+
entries: selected.entries,
|
|
39027
39147
|
omitted: selected.omitted === 0 ? undefined : selected.omitted
|
|
39028
39148
|
});
|
|
39029
39149
|
}
|
|
@@ -39045,7 +39165,7 @@ class GodotServer {
|
|
|
39045
39165
|
run.endedUnwatched = noCodeWillCome(run);
|
|
39046
39166
|
}
|
|
39047
39167
|
const severity = readString(args, "severity");
|
|
39048
|
-
const selected = run.log.
|
|
39168
|
+
const selected = run.log.selectFolded({
|
|
39049
39169
|
severity: severity === "error" || severity === "warning" ? severity : "info",
|
|
39050
39170
|
sinceLastCall: readBoolean(args, "sinceLastCall") ?? false,
|
|
39051
39171
|
contains: readNonEmptyString(args, "contains"),
|
|
@@ -39111,7 +39231,7 @@ class GodotServer {
|
|
|
39111
39231
|
cpuSeconds,
|
|
39112
39232
|
note: notes.length > 0 ? notes.join(" ") : undefined,
|
|
39113
39233
|
omitted: selected.omitted,
|
|
39114
|
-
entries:
|
|
39234
|
+
entries: selected.entries
|
|
39115
39235
|
});
|
|
39116
39236
|
}
|
|
39117
39237
|
anotherProjectsRun(runsProject, args, act) {
|
|
@@ -39178,7 +39298,7 @@ class GodotServer {
|
|
|
39178
39298
|
clean: verdict.clean,
|
|
39179
39299
|
errorsUnread: errorsUnread(stopped) ? true : undefined,
|
|
39180
39300
|
note: verdict.withChildren ? `${verdict.note} ${aboutTheChildren(ended, stopped.throughEditor)}` : verdict.note,
|
|
39181
|
-
|
|
39301
|
+
...foldedEntriesAnswer(stopped.log)
|
|
39182
39302
|
});
|
|
39183
39303
|
}
|
|
39184
39304
|
async stopAnnouncedGame(pid, andChildren, args) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.56",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|